diff --git a/cmd/common/json.go b/cmd/common/json.go index b14eb038..e3436aa7 100644 --- a/cmd/common/json.go +++ b/cmd/common/json.go @@ -46,7 +46,7 @@ func JSONMarshalKey(k interface{}) (keyJSON []byte, err error) { // Marshal string or Base64 otherwise. keyJSON, err = json.Marshal(k) } - return + return keyJSON, err } // JSONPrettyPrintRoflAppConfig is a wrapper around rofl.AppConfig that implements custom JSON marshaling. diff --git a/cmd/common/selector.go b/cmd/common/selector.go index c625a28b..734ad8f7 100644 --- a/cmd/common/selector.go +++ b/cmd/common/selector.go @@ -110,7 +110,7 @@ func (npa *NPASelection) PrettyPrintNetwork() (out string) { if len(npa.Network.Description) > 0 { out += fmt.Sprintf(" (%s)", npa.Network.Description) } - return + return out } // ConsensusDenomination returns the denomination used to represent the consensus layer token. @@ -125,7 +125,7 @@ func (npa *NPASelection) ConsensusDenomination() (denom types.Denomination) { default: denom = types.Denomination(cfgDenom) } - return + return denom } func init() { diff --git a/cmd/rofl/common/flags.go b/cmd/rofl/common/flags.go index 77084a5e..c9b29bb6 100644 --- a/cmd/rofl/common/flags.go +++ b/cmd/rofl/common/flags.go @@ -19,6 +19,9 @@ var ( // TermFlags provide the term and count setting. TermFlags *flag.FlagSet + // ShowOffersFlag is the flag for showing all provider offers. + ShowOffersFlag *flag.FlagSet + // DeploymentName is the name of the ROFL app deployment. DeploymentName string @@ -33,6 +36,9 @@ var ( // TermCount specific the rental base unit multiplier. TermCount uint64 + + // ShowOffers controls whether to display all offers for each provider. + ShowOffers bool ) func init() { @@ -48,4 +54,7 @@ func init() { TermFlags = flag.NewFlagSet("", flag.ContinueOnError) TermFlags.StringVar(&Term, "term", "", "term to pay for in advance [hour, month, year]") TermFlags.Uint64Var(&TermCount, "term-count", 1, "number of terms to pay for in advance") + + ShowOffersFlag = flag.NewFlagSet("", flag.ContinueOnError) + ShowOffersFlag.BoolVar(&ShowOffers, "show-offers", false, "show all offers for each provider") } diff --git a/cmd/rofl/common/term.go b/cmd/rofl/common/term.go index 1518ec7a..f94baa3e 100644 --- a/cmd/rofl/common/term.go +++ b/cmd/rofl/common/term.go @@ -31,3 +31,43 @@ func ParseMachineTerm(term string) roflmarket.Term { return 0 } } + +// FormatTerm formats a roflmarket.Term into a human-readable string. +func FormatTerm(term roflmarket.Term) string { + switch term { + case roflmarket.TermHour: + return TermHour + case roflmarket.TermMonth: + return TermMonth + case roflmarket.TermYear: + return TermYear + default: + return fmt.Sprintf("", term) + } +} + +// FormatTermAdjectival formats a roflmarket.Term into an adjectival form (e.g., "hourly"). +func FormatTermAdjectival(term roflmarket.Term) string { + switch term { + case roflmarket.TermHour: + return "hourly" + case roflmarket.TermMonth: + return "monthly" + case roflmarket.TermYear: + return "yearly" + default: + return fmt.Sprintf("term_%d", term) + } +} + +// FormatTeeType formats a roflmarket.TeeType into a human-readable string. +func FormatTeeType(tee roflmarket.TeeType) string { + switch tee { + case roflmarket.TeeTypeSGX: + return "sgx" + case roflmarket.TeeTypeTDX: + return "tdx" + default: + return fmt.Sprintf("", tee) + } +} diff --git a/cmd/rofl/deploy.go b/cmd/rofl/deploy.go index dff29e51..0ce37124 100644 --- a/cmd/rofl/deploy.go +++ b/cmd/rofl/deploy.go @@ -32,6 +32,7 @@ import ( "github.com/oasisprotocol/cli/cmd/common" roflCmdBuild "github.com/oasisprotocol/cli/cmd/rofl/build" roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" + roflProvider "github.com/oasisprotocol/cli/cmd/rofl/provider" cliConfig "github.com/oasisprotocol/cli/config" ) @@ -40,7 +41,6 @@ var ( deployOffer string deployMachine string deployForce bool - deployShowOffers bool deployReplaceMachine bool deployCmd = &cobra.Command{ @@ -129,9 +129,17 @@ var ( fmt.Printf("Using provider: %s (%s)\n", machine.Provider, providerAddr) - if deployShowOffers { + if roflCommon.ShowOffers { // Display all offers supported by the provider. - showProviderOffers(ctx, npa, conn, *providerAddr) + offers, err := fetchProviderOffers(ctx, npa, conn, *providerAddr) + cobra.CheckErr(err) + + fmt.Println() + fmt.Printf("Offers available from the selected provider:\n") + for _, offer := range offers { + roflProvider.ShowOfferSummary(npa, offer) + } + fmt.Println() return } @@ -183,12 +191,17 @@ var ( } } if offer == nil { - showProviderOffers(ctx, npa, conn, *providerAddr) + fmt.Println() + fmt.Printf("Offers available from the selected provider:\n") + for _, of := range offers { + roflProvider.ShowOfferSummary(npa, of) + } + fmt.Println() return nil, nil, fmt.Errorf("offer '%s' not found for provider '%s'", machine.Offer, providerAddr) } fmt.Printf("Taking offer:\n") - showProviderOffer(ctx, offer) + roflProvider.ShowOfferSummary(npa, offer) term := detectTerm(offer) if roflCommon.TermCount < 1 { @@ -203,7 +216,7 @@ var ( } cobra.CheckErr(totalPrice.Mul(qTermCount)) tp := types.NewBaseUnits(totalPrice, offer.Payment.Native.Denomination) - fmt.Printf("Selected per-%s pricing term, total price is ", term2str(term)) + fmt.Printf("Selected per-%s pricing term, total price is ", roflCommon.FormatTerm(term)) tp.PrettyPrint(ctx, "", os.Stdout) fmt.Println(".") // Warn the user about the non-refundable rental policy before first renting. @@ -350,7 +363,7 @@ func pushBundleToOciRepository(orcFilename string, ociRepository string) (string func detectTerm(offer *roflmarket.Offer) (term roflmarket.Term) { if offer == nil { cobra.CheckErr(fmt.Errorf("no offers exist to determine payment term")) - return // Linter complains otherwise. + return term // Linter complains otherwise. } if offer.Payment.Native == nil { cobra.CheckErr(fmt.Errorf("no payment terms available for offer '%s'", offer.ID)) @@ -362,7 +375,7 @@ func detectTerm(offer *roflmarket.Offer) (term roflmarket.Term) { if _, ok := offer.Payment.Native.Terms[term]; !ok { cobra.CheckErr(fmt.Errorf("term '%s' is not available for offer '%s'", roflCommon.Term, offer.ID)) } - return + return term } // Take the longest payment period. @@ -372,108 +385,20 @@ func detectTerm(offer *roflmarket.Offer) (term roflmarket.Term) { term = t } } - return + return term } func fetchProviderOffers(ctx context.Context, npa *common.NPASelection, conn connection.Connection, provider types.Address) (offers []*roflmarket.Offer, err error) { offers, err = conn.Runtime(npa.ParaTime).ROFLMarket.Offers(ctx, client.RoundLatest, provider) if err != nil { err = fmt.Errorf("failed to query provider: %s", err) - return + return offers, err } // Order offers, newer first. sort.Slice(offers, func(i, j int) bool { return bytes.Compare(offers[i].ID[:], offers[j].ID[:]) > 0 }) - return -} - -func showProviderOffers(ctx context.Context, npa *common.NPASelection, conn connection.Connection, provider types.Address) { - offers, err := fetchProviderOffers(ctx, npa, conn, provider) - cobra.CheckErr(err) - - fmt.Println() - fmt.Printf("Offers available from the selected provider:\n") - for idx, offer := range offers { - showProviderOffer(ctx, offer) - if idx != len(offers)-1 { - fmt.Println() - } - } - fmt.Println() -} - -func showProviderOffer(ctx context.Context, offer *roflmarket.Offer) { - name, ok := offer.Metadata[provider.SchedulerMetadataOfferKey] - if !ok { - name = "" - } - - var tee string - switch offer.Resources.TEE { - case roflmarket.TeeTypeSGX: - tee = "sgx" - case roflmarket.TeeTypeTDX: - tee = "tdx" - default: - tee = "" - } - - fmt.Printf("- %s [%s]\n", name, offer.ID) - fmt.Printf(" TEE: %s | Memory: %d MiB | vCPUs: %d | Storage: %.2f GiB\n", - tee, - offer.Resources.Memory, - offer.Resources.CPUCount, - float64(offer.Resources.Storage)/1024., - ) - if _, ok := offer.Metadata[provider.NoteMetadataKey]; ok { - fmt.Printf(" Note: %s\n", offer.Metadata[provider.NoteMetadataKey]) - } - if _, ok := offer.Metadata[provider.DescriptionMetadataKey]; ok { - fmt.Printf(" Description:\n %s\n", strings.ReplaceAll(offer.Metadata[provider.DescriptionMetadataKey], "\n", "\n ")) - } - if offer.Payment.Native != nil { - if len(offer.Payment.Native.Terms) == 0 { - return - } - - // Specify sorting order for terms. - terms := []roflmarket.Term{roflmarket.TermHour, roflmarket.TermMonth, roflmarket.TermYear} - - // Go through provided payment terms and print price. - fmt.Printf(" Price: ") - var gotPrev bool - for _, term := range terms { - price, exists := offer.Payment.Native.Terms[term] - if !exists { - continue - } - - if gotPrev { - fmt.Printf(" | ") - } - - bu := types.NewBaseUnits(price, offer.Payment.Native.Denomination) - bu.PrettyPrint(ctx, "", os.Stdout) - fmt.Printf("/%s", term2str(term)) - gotPrev = true - } - fmt.Println() - } -} - -// Helper to convert roflmarket term into string. -func term2str(term roflmarket.Term) string { - switch term { - case roflmarket.TermHour: - return "hour" - case roflmarket.TermMonth: - return "month" - case roflmarket.TermYear: - return "year" - default: - return "" - } + return offers, err } func resolveAndMarshalPermissions(npa *common.NPASelection, permissions map[string][]string) (string, error) { @@ -514,12 +439,12 @@ func init() { providerFlags.StringVar(&deployOffer, "offer", "", "set the provider's offer identifier") providerFlags.StringVar(&deployMachine, "machine", buildRofl.DefaultMachineName, "machine to deploy into") providerFlags.BoolVar(&deployForce, "force", false, "force deployment") - providerFlags.BoolVar(&deployShowOffers, "show-offers", false, "show all provider offers and quit") providerFlags.BoolVar(&deployReplaceMachine, "replace-machine", false, "rent a new machine if the provided one expired") deployCmd.Flags().AddFlagSet(common.AccountFlag) deployCmd.Flags().AddFlagSet(common.RuntimeTxFlags) deployCmd.Flags().AddFlagSet(providerFlags) + deployCmd.Flags().AddFlagSet(roflCommon.ShowOffersFlag) deployCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) deployCmd.Flags().AddFlagSet(roflCommon.WipeFlags) deployCmd.Flags().AddFlagSet(roflCommon.TermFlags) diff --git a/cmd/rofl/provider/list.go b/cmd/rofl/provider/list.go new file mode 100644 index 00000000..77e3252a --- /dev/null +++ b/cmd/rofl/provider/list.go @@ -0,0 +1,224 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/roflmarket" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" + + "github.com/oasisprotocol/cli/build/rofl/provider" + "github.com/oasisprotocol/cli/cmd/common" + roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" + cliConfig "github.com/oasisprotocol/cli/config" + "github.com/oasisprotocol/cli/table" +) + +var listCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List ROFL providers", + Long: `List all ROFL providers registered on the selected paratime. + +This command queries on-chain provider data and displays provider addresses, +scheduler app IDs, node counts, and offer/instance counts. + +Use --show-offers to expand and display all offers for each provider. +Use --format json for machine-readable output.`, + Args: cobra.NoArgs, + Run: func(_ *cobra.Command, _ []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + npa.MustHaveParaTime() + + ctx := context.Background() + conn, err := connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + + // Query all providers from the chain. + providers, err := conn.Runtime(npa.ParaTime).ROFLMarket.Providers(ctx, client.RoundLatest) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to query providers: %w", err)) + } + + if len(providers) == 0 { + fmt.Println("No providers found.") + return + } + + // Sort providers by address for consistent output. + sort.Slice(providers, func(i, j int) bool { + return providers[i].Address.String() < providers[j].Address.String() + }) + + // Output format handling. + if common.OutputFormat() == common.FormatJSON { + outputJSON(ctx, npa, conn, providers) + } else { + outputText(ctx, npa, conn, providers) + } + }, +} + +// outputJSON returns providers in JSON format. +func outputJSON(ctx context.Context, npa *common.NPASelection, conn connection.Connection, providers []*roflmarket.Provider) { + type ProviderWithOffers struct { + *roflmarket.Provider + Offers []*roflmarket.Offer `json:"offers,omitempty"` + } + + output := make([]ProviderWithOffers, 0, len(providers)) + + for _, provider := range providers { + pwo := ProviderWithOffers{Provider: provider} + + if roflCommon.ShowOffers { + offers, err := conn.Runtime(npa.ParaTime).ROFLMarket.Offers(ctx, client.RoundLatest, provider.Address) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to query offers for provider %s: %w", provider.Address, err)) + } + pwo.Offers = offers + } + + output = append(output, pwo) + } + + data, err := json.MarshalIndent(output, "", " ") + cobra.CheckErr(err) + fmt.Printf("%s\n", data) +} + +// outputText returns providers in human-readable table format. +func outputText(ctx context.Context, npa *common.NPASelection, conn connection.Connection, providers []*roflmarket.Provider) { + table := table.New() + table.SetHeader([]string{"Provider Address", "Scheduler App", "Nodes", "Offers", "Instances"}) + + rows := make([][]string, 0, len(providers)) + for _, provider := range providers { + // Format node count. + var nodesList string + if len(provider.Nodes) == 0 { + nodesList = "0" + } else { + nodesList = fmt.Sprintf("%d", len(provider.Nodes)) + } + + rows = append(rows, []string{ + provider.Address.String(), + provider.SchedulerApp.String(), + nodesList, + fmt.Sprintf("%d", provider.OffersCount), + fmt.Sprintf("%d", provider.InstancesCount), + }) + } + + table.AppendBulk(rows) + table.Render() + + // If --show-offers is enabled, display offers for each provider. + if roflCommon.ShowOffers { + fmt.Println() + for _, provider := range providers { + showProviderOffersExpanded(ctx, npa, conn, provider) + } + } +} + +// showProviderOffersExpanded returns all offers for a given provider with expanded display. +func showProviderOffersExpanded(ctx context.Context, npa *common.NPASelection, conn connection.Connection, provider *roflmarket.Provider) { + offers, err := conn.Runtime(npa.ParaTime).ROFLMarket.Offers(ctx, client.RoundLatest, provider.Address) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to query offers for provider %s: %w", provider.Address, err)) + } + + if len(offers) == 0 { + fmt.Printf("Provider %s: No offers\n", provider.Address) + return + } + + // Sort offers by ID for consistent output. + sort.Slice(offers, func(i, j int) bool { + for k := 0; k < 8; k++ { + if offers[i].ID[k] != offers[j].ID[k] { + return offers[i].ID[k] < offers[j].ID[k] + } + } + return false + }) + + fmt.Printf("Provider %s (%d offers):\n", provider.Address, len(offers)) + for _, offer := range offers { + ShowOfferSummary(npa, offer) + } + fmt.Println() +} + +// ShowOfferSummary outputs a summary of a single offer. +func ShowOfferSummary(npa *common.NPASelection, offer *roflmarket.Offer) { + // Extract offer name from metadata if available. + name, ok := offer.Metadata[provider.SchedulerMetadataOfferKey] + if !ok { + name = "" + } + + // Determine TEE type. + tee := roflCommon.FormatTeeType(offer.Resources.TEE) + + // Format GPU info if present. + var gpu string + if offer.Resources.GPU != nil { + gpu = fmt.Sprintf(" | GPU: %d", offer.Resources.GPU.Count) + if offer.Resources.GPU.Model != "" { + gpu += fmt.Sprintf(" (%s)", offer.Resources.GPU.Model) + } + } + + fmt.Printf(" - %s [%s]\n", name, offer.ID) + fmt.Printf(" TEE: %s | Memory: %d MiB | vCPUs: %d | Storage: %.2f GiB%s\n", + tee, + offer.Resources.Memory, + offer.Resources.CPUCount, + float64(offer.Resources.Storage)/1024., + gpu, + ) + fmt.Printf(" Capacity: %d\n", offer.Capacity) + + // Note and Description from metadata. + if note, ok := offer.Metadata[provider.NoteMetadataKey]; ok { + fmt.Printf(" Note: %s\n", note) + } + if desc, ok := offer.Metadata[provider.DescriptionMetadataKey]; ok { + fmt.Printf(" Description:\n %s\n", strings.ReplaceAll(desc, "\n", "\n ")) + } + + // Payment info. + switch { + case offer.Payment.Native != nil: + if len(offer.Payment.Native.Terms) > 0 { + var terms []string + for term, amount := range offer.Payment.Native.Terms { + bu := types.NewBaseUnits(amount, offer.Payment.Native.Denomination) + formattedAmount := helpers.FormatParaTimeDenomination(npa.ParaTime, bu) + terms = append(terms, fmt.Sprintf("%s: %s", roflCommon.FormatTermAdjectival(term), formattedAmount)) + } + sort.Strings(terms) + fmt.Printf(" Payment: %s\n", strings.Join(terms, ", ")) + } + case offer.Payment.EvmContract != nil: + fmt.Printf(" Payment: EVM Contract (0x%x)\n", offer.Payment.EvmContract.Address[:]) + } +} + +func init() { + listCmd.Flags().AddFlagSet(roflCommon.ShowOffersFlag) + listCmd.Flags().AddFlagSet(common.SelectorNPFlags) + listCmd.Flags().AddFlagSet(common.FormatFlag) +} diff --git a/cmd/rofl/provider/provider.go b/cmd/rofl/provider/provider.go index 9c696f9a..e60c2741 100644 --- a/cmd/rofl/provider/provider.go +++ b/cmd/rofl/provider/provider.go @@ -16,4 +16,6 @@ func init() { Cmd.AddCommand(updateCmd) Cmd.AddCommand(updateOffersCmd) Cmd.AddCommand(removeCmd) + Cmd.AddCommand(listCmd) + Cmd.AddCommand(showCmd) } diff --git a/cmd/rofl/provider/show.go b/cmd/rofl/provider/show.go new file mode 100644 index 00000000..c5b0a3c1 --- /dev/null +++ b/cmd/rofl/provider/show.go @@ -0,0 +1,159 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/roflmarket" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" + + "github.com/oasisprotocol/cli/cmd/common" + cliConfig "github.com/oasisprotocol/cli/config" +) + +var showCmd = &cobra.Command{ + Use: "show
", + Short: "Show details of a ROFL provider", + Long: `Show detailed information about a specific ROFL provider. + +This command queries on-chain provider data and displays all provider details +including address, scheduler app, nodes, payment address, and all offers. + +Use --format json for machine-readable output including provider metadata.`, + Args: cobra.ExactArgs(1), + Run: func(_ *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + npa.MustHaveParaTime() + + ctx := context.Background() + conn, err := connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + + // Parse provider address. + providerAddr := args[0] + var addr types.Address + if err = addr.UnmarshalText([]byte(providerAddr)); err != nil { + cobra.CheckErr(fmt.Errorf("invalid provider address '%s': %w", providerAddr, err)) + } + + // Query the provider from the chain. + provider, err := conn.Runtime(npa.ParaTime).ROFLMarket.Provider(ctx, client.RoundLatest, addr) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to query provider: %w", err)) + } + + // Query offers for the provider. + offers, err := conn.Runtime(npa.ParaTime).ROFLMarket.Offers(ctx, client.RoundLatest, addr) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to query offers for provider: %w", err)) + } + + // Output format handling. + if common.OutputFormat() == common.FormatJSON { + outputProviderJSON(provider, offers) + } else { + outputProviderText(npa, provider, offers) + } + }, +} + +// outputProviderJSON outputs provider details in JSON format. +func outputProviderJSON(provider *roflmarket.Provider, offers []*roflmarket.Offer) { + type ProviderWithOffers struct { + *roflmarket.Provider + Offers []*roflmarket.Offer `json:"offers"` + } + + output := ProviderWithOffers{ + Provider: provider, + Offers: offers, + } + + data, err := json.MarshalIndent(output, "", " ") + cobra.CheckErr(err) + fmt.Printf("%s\n", data) +} + +// outputProviderText outputs provider details in human-readable format. +func outputProviderText(npa *common.NPASelection, provider *roflmarket.Provider, offers []*roflmarket.Offer) { + fmt.Printf("Provider: %s\n", provider.Address) + fmt.Println() + + // Basic information. + fmt.Println("=== Basic Information ===") + fmt.Printf("Scheduler App: %s\n", provider.SchedulerApp) + + // Payment address. + var paymentAddr string + switch { + case provider.PaymentAddress.Native != nil: + paymentAddr = provider.PaymentAddress.Native.String() + case provider.PaymentAddress.Eth != nil: + paymentAddr = fmt.Sprintf("0x%x", provider.PaymentAddress.Eth[:]) + default: + paymentAddr = "" + } + fmt.Printf("Payment Address: %s\n", paymentAddr) + + // Nodes. + fmt.Printf("Nodes: ") + if len(provider.Nodes) == 0 { + fmt.Println("") + } else { + fmt.Printf("%d\n", len(provider.Nodes)) + for i, node := range provider.Nodes { + fmt.Printf(" %d. %s\n", i+1, node) + } + } + + // Stake. + stake := helpers.FormatParaTimeDenomination(npa.ParaTime, provider.Stake) + fmt.Printf("Stake: %s\n", stake) + + // Counts. + fmt.Printf("Offers: %d\n", provider.OffersCount) + fmt.Printf("Instances: %d\n", provider.InstancesCount) + + // Timestamps. + if provider.CreatedAt > 0 { + fmt.Printf("Created At: %s\n", time.Unix(int64(provider.CreatedAt), 0).UTC().Format(time.RFC3339)) //nolint:gosec + } + if provider.UpdatedAt > 0 { + fmt.Printf("Updated At: %s\n", time.Unix(int64(provider.UpdatedAt), 0).UTC().Format(time.RFC3339)) //nolint:gosec + } + + // Offers. + fmt.Println() + fmt.Println("=== Offers ===") + if len(offers) == 0 { + fmt.Println("") + } else { + // Sort offers by ID for consistent output. + sort.Slice(offers, func(i, j int) bool { + for k := 0; k < 8; k++ { + if offers[i].ID[k] != offers[j].ID[k] { + return offers[i].ID[k] < offers[j].ID[k] + } + } + return false + }) + + for _, offer := range offers { + ShowOfferSummary(npa, offer) + } + } +} + +func init() { + showCmd.Flags().AddFlagSet(common.SelectorNPFlags) + showCmd.Flags().AddFlagSet(common.FormatFlag) +} diff --git a/docs/rofl.md b/docs/rofl.md index c29558cb..f4b08d0e 100644 --- a/docs/rofl.md +++ b/docs/rofl.md @@ -431,6 +431,42 @@ in your provider manifest file. To update your provider policies, run [`rofl provider update`](#provider-update) instead. +#### List ROFL providers {#provider-list} + +Use `rofl provider list` to display all ROFL providers registered on the +selected ParaTime: + +![code shell](../examples/rofl/provider-list.in.static) + +![code](../examples/rofl/provider-list.out.static) + +The command displays provider addresses, scheduler app IDs, node counts, and +offer/instance counts for each provider. + +To see detailed information about all offers from each provider, use the +`--show-offers` flag: + +![code shell](../examples/rofl/provider-list-show-offers.in.static) + +#### Show ROFL provider details {#provider-show} + +Use `rofl provider show
` to display detailed information about a +specific ROFL provider, including all their offers: + +![code shell](../examples/rofl/provider-show.in.static) + +![code](../examples/rofl/provider-show.out.static) + +This command provides comprehensive information including: + +- Basic provider information (address, scheduler app, payment address) +- List of endorsed nodes +- Stake amount +- Detailed information about all offers (resources, pricing terms, capacity) + +Use `--format json` to get the full provider metadata in machine-readable +format. + #### Remove ROFL provider from the network {#provider-remove} Run `rofl provider remove` to deregister your ROFL provider account: diff --git a/examples/rofl/provider-list-json.in.static b/examples/rofl/provider-list-json.in.static new file mode 100644 index 00000000..61faff34 --- /dev/null +++ b/examples/rofl/provider-list-json.in.static @@ -0,0 +1 @@ +oasis rofl provider list --format json diff --git a/examples/rofl/provider-list-json.out.static b/examples/rofl/provider-list-json.out.static new file mode 100644 index 00000000..e591c3b1 --- /dev/null +++ b/examples/rofl/provider-list-json.out.static @@ -0,0 +1,149 @@ +[ + { + "address": "oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz", + "nodes": [ + "bOlqho9R3JHP64kJk+SfMxZt5fNkYWf6gdhErWlY60E=", + "1owPK3eT21k0ajRG7VfHRgp4JPXobCQtzuglz6ZSJis=" + ], + "scheduler_app": "rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg", + "payment_address": { + "native": "oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz" + }, + "metadata": { + "net.oasis.provider.homepage": "https://oasis.net", + "net.oasis.provider.name": "OPF Testnet" + }, + "stake": { + "Amount": "100000000000000000000", + "Denomination": "" + }, + "offers_next_id": "0000000000000004", + "offers_count": 1, + "instances_next_id": "0000000000000423", + "instances_count": 12, + "created_at": 1745405831, + "updated_at": 1763564052 + }, + { + "address": "oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3", + "nodes": [ + "mXsy6XlJlEK5vJwEfyqRWZLVN5Ss4QpwI6h124IDjjw=" + ], + "scheduler_app": "rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg", + "payment_address": { + "native": "oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3" + }, + "metadata": {}, + "stake": { + "Amount": "100000000000000000000", + "Denomination": "" + }, + "offers_next_id": "0000000000000003", + "offers_count": 3, + "instances_next_id": "0000000000000000", + "instances_count": 0, + "created_at": 1763242822, + "updated_at": 1763242822 + }, + { + "address": "oasis1qrcxr6lh03xyazkg7ad7q2dqs94kj0arusmyzq8g", + "nodes": [], + "scheduler_app": "rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg", + "payment_address": { + "native": "oasis1qrcxr6lh03xyazkg7ad7q2dqs94kj0arusmyzq8g" + }, + "metadata": {}, + "stake": { + "Amount": "100000000000000000000", + "Denomination": "" + }, + "offers_next_id": "0000000000000000", + "offers_count": 0, + "instances_next_id": "0000000000000000", + "instances_count": 0, + "created_at": 1748354255, + "updated_at": 1748354255 + }, + { + "address": "oasis1qrfeadn03ljm0kfx8wx0d5zf6kj79pxqvv0dukdm", + "nodes": [ + "ULUybf1RbPkDWUT/m/qPKT+f6WOtnZyiugDM4R6Nfm8=" + ], + "scheduler_app": "rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg", + "payment_address": { + "native": "oasis1qrfeadn03ljm0kfx8wx0d5zf6kj79pxqvv0dukdm" + }, + "metadata": { + "net.oasis.provider.name": "OPF Testnet Provider A" + }, + "stake": { + "Amount": "100000000000000000000", + "Denomination": "" + }, + "offers_next_id": "0000000000000001", + "offers_count": 1, + "instances_next_id": "0000000000000009", + "instances_count": 2, + "created_at": 1743595118, + "updated_at": 1759299287 + }, + { + "address": "oasis1qrjprejadvxjwj3m3mj8xurt0mvafw4jhymmmtlj", + "nodes": [], + "scheduler_app": "rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg", + "payment_address": { + "native": "oasis1qrjprejadvxjwj3m3mj8xurt0mvafw4jhymmmtlj" + }, + "metadata": {}, + "stake": { + "Amount": "100000000000000000000", + "Denomination": "" + }, + "offers_next_id": "0000000000000000", + "offers_count": 0, + "instances_next_id": "0000000000000000", + "instances_count": 0, + "created_at": 1746798928, + "updated_at": 1746798928 + }, + { + "address": "oasis1qrpptdcpsxvxn3re0cg3f6hfy0kyfujnz5ex7vgn", + "nodes": [], + "scheduler_app": "rofl1qr95suussttd2g9ehu3zcpgx8ewtwgayyuzsl0x2", + "payment_address": { + "native": "oasis1qrpptdcpsxvxn3re0cg3f6hfy0kyfujnz5ex7vgn" + }, + "metadata": {}, + "stake": { + "Amount": "100000000000000000000", + "Denomination": "" + }, + "offers_next_id": "0000000000000002", + "offers_count": 2, + "instances_next_id": "0000000000000002", + "instances_count": 2, + "created_at": 1758443738, + "updated_at": 1758444736 + }, + { + "address": "oasis1qrxhk2aqwq7g5fq85a89yv2khdgn2wzccqhg2sal", + "nodes": [ + "5MsgQwijUlpH9+0Hbyors5jwmx7tTmKMA4c9leV3prI=" + ], + "scheduler_app": "rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg", + "payment_address": { + "native": "oasis1qrxhk2aqwq7g5fq85a89yv2khdgn2wzccqhg2sal" + }, + "metadata": {}, + "stake": { + "Amount": "100000000000000000000", + "Denomination": "" + }, + "offers_next_id": "0000000000000005", + "offers_count": 4, + "instances_next_id": "000000000000001d", + "instances_count": 0, + "created_at": 1744628473, + "updated_at": 1761127084 + } +] diff --git a/examples/rofl/provider-list-show-offers.in.static b/examples/rofl/provider-list-show-offers.in.static new file mode 100644 index 00000000..3504745d --- /dev/null +++ b/examples/rofl/provider-list-show-offers.in.static @@ -0,0 +1 @@ +oasis rofl provider list --show-offers diff --git a/examples/rofl/provider-list-show-offers.out.static b/examples/rofl/provider-list-show-offers.out.static new file mode 100644 index 00000000..9a5d154c --- /dev/null +++ b/examples/rofl/provider-list-show-offers.out.static @@ -0,0 +1,85 @@ +PROVIDER ADDRESS SCHEDULER APP NODES OFFERS INSTANCES +oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 2 1 8 +oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3 rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 1 3 0 +oasis1qrcxr6lh03xyazkg7ad7q2dqs94kj0arusmyzq8g rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 0 0 0 +oasis1qrfeadn03ljm0kfx8wx0d5zf6kj79pxqvv0dukdm rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 1 1 2 +oasis1qrjprejadvxjwj3m3mj8xurt0mvafw4jhymmmtlj rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 0 0 0 +oasis1qrpptdcpsxvxn3re0cg3f6hfy0kyfujnz5ex7vgn rofl1qr95suussttd2g9ehu3zcpgx8ewtwgayyuzsl0x2 0 2 2 +oasis1qrxhk2aqwq7g5fq85a89yv2khdgn2wzccqhg2sal rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 1 4 0 + +Provider oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz (1 offers): + - playground_short [0000000000000003] + TEE: tdx | Memory: 4096 MiB | vCPUs: 2 | Storage: 19.53 GiB + Capacity: 203 + Note: ⚠️ Testnet ROFLs only. Do not use in production! ⚠️ + Description: + Demo machine suitable for oracles, AI agents and light-weight web apps. + If you are running a local LLM, make sure it fits main memory (e.g. + gemma3:1b, deepseek-r1:1.5b)! + Payment: hourly: 5.0 TEST + +Provider oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3 (3 offers): + - small [0000000000000000] + TEE: tdx | Memory: 8192 MiB | vCPUs: 2 | Storage: 39.06 GiB + Capacity: 10 + Note: Small instance - ideal for lightweight ROFL applications + Description: + Small compute instance with 2 vCPUs, 8GB RAM, and 40GB storage. + Perfect for testing and lightweight ROFL applications. + Hosted on Akash decentralized cloud infrastructure. + Payment: monthly: 150.0 TEST + - medium [0000000000000001] + TEE: tdx | Memory: 16384 MiB | vCPUs: 4 | Storage: 78.12 GiB + Capacity: 5 + Note: Medium instance - balanced compute and memory + Description: + Medium compute instance with 4 vCPUs, 16GB RAM, and 80GB storage. + Great for standard ROFL applications with moderate resource needs. + Hosted on Akash decentralized cloud infrastructure. + Payment: monthly: 300.0 TEST + - large [0000000000000002] + TEE: tdx | Memory: 28672 MiB | vCPUs: 8 | Storage: 175.78 GiB + Capacity: 1 + Note: Large instance - high-performance computing + Description: + Large compute instance with 8 vCPUs, 28GB RAM, and 180GB storage. + Designed for resource-intensive ROFL applications. + Hosted on Akash decentralized cloud infrastructure. + Payment: monthly: 600.0 TEST + +Provider oasis1qrcxr6lh03xyazkg7ad7q2dqs94kj0arusmyzq8g: No offers +Provider oasis1qrfeadn03ljm0kfx8wx0d5zf6kj79pxqvv0dukdm (1 offers): + - test [0000000000000000] + TEE: tdx | Memory: 512 MiB | vCPUs: 1 | Storage: 10.00 GiB + Capacity: 12 + Payment: monthly: 100.0 TEST + +Provider oasis1qrjprejadvxjwj3m3mj8xurt0mvafw4jhymmmtlj: No offers +Provider oasis1qrpptdcpsxvxn3re0cg3f6hfy0kyfujnz5ex7vgn (2 offers): + - myaccount [0000000000000000] + TEE: tdx | Memory: 512 MiB | vCPUs: 1 | Storage: 0.50 GiB + Capacity: 10 + Payment: hourly: 1.0 TEST + - medium [0000000000000001] + TEE: tdx | Memory: 1024 MiB | vCPUs: 2 | Storage: 1.00 GiB + Capacity: 5 + Payment: hourly: 5.0 TEST + +Provider oasis1qrxhk2aqwq7g5fq85a89yv2khdgn2wzccqhg2sal (4 offers): + - small [0000000000000001] + TEE: tdx | Memory: 1024 MiB | vCPUs: 1 | Storage: 9.77 GiB + Capacity: 11 + Payment: monthly: 10.0 TEST + - medium [0000000000000002] + TEE: tdx | Memory: 2048 MiB | vCPUs: 1 | Storage: 14.65 GiB + Capacity: 11 + Payment: monthly: 20.0 TEST + - large [0000000000000003] + TEE: tdx | Memory: 4096 MiB | vCPUs: 2 | Storage: 29.30 GiB + Capacity: 6 + Payment: monthly: 50.0 TEST + - small_sgx [0000000000000004] + TEE: sgx | Memory: 1024 MiB | vCPUs: 1 | Storage: 9.77 GiB + Capacity: 10 + Payment: monthly: 10.0 TEST + diff --git a/examples/rofl/provider-list.in.static b/examples/rofl/provider-list.in.static new file mode 100644 index 00000000..29f06f3a --- /dev/null +++ b/examples/rofl/provider-list.in.static @@ -0,0 +1 @@ +oasis rofl provider list diff --git a/examples/rofl/provider-list.out.static b/examples/rofl/provider-list.out.static new file mode 100644 index 00000000..4562083c --- /dev/null +++ b/examples/rofl/provider-list.out.static @@ -0,0 +1,8 @@ +PROVIDER ADDRESS SCHEDULER APP NODES OFFERS INSTANCES +oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 2 1 12 +oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3 rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 1 3 0 +oasis1qrcxr6lh03xyazkg7ad7q2dqs94kj0arusmyzq8g rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 0 0 0 +oasis1qrfeadn03ljm0kfx8wx0d5zf6kj79pxqvv0dukdm rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 1 1 2 +oasis1qrjprejadvxjwj3m3mj8xurt0mvafw4jhymmmtlj rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 0 0 0 +oasis1qrpptdcpsxvxn3re0cg3f6hfy0kyfujnz5ex7vgn rofl1qr95suussttd2g9ehu3zcpgx8ewtwgayyuzsl0x2 0 2 2 +oasis1qrxhk2aqwq7g5fq85a89yv2khdgn2wzccqhg2sal rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg 1 4 0 diff --git a/examples/rofl/provider-show-json.in.static b/examples/rofl/provider-show-json.in.static new file mode 100644 index 00000000..7110b28e --- /dev/null +++ b/examples/rofl/provider-show-json.in.static @@ -0,0 +1 @@ +oasis rofl provider show oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3 --format json diff --git a/examples/rofl/provider-show-json.out.static b/examples/rofl/provider-show-json.out.static new file mode 100644 index 00000000..ce816af5 --- /dev/null +++ b/examples/rofl/provider-show-json.out.static @@ -0,0 +1,92 @@ +{ + "address": "oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3", + "nodes": [ + "mXsy6XlJlEK5vJwEfyqRWZLVN5Ss4QpwI6h124IDjjw=" + ], + "scheduler_app": "rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg", + "payment_address": { + "native": "oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3" + }, + "metadata": {}, + "stake": { + "Amount": "100000000000000000000", + "Denomination": "" + }, + "offers_next_id": "0000000000000003", + "offers_count": 3, + "instances_next_id": "0000000000000000", + "instances_count": 0, + "created_at": 1763242822, + "updated_at": 1763242822, + "offers": [ + { + "id": "0000000000000000", + "resources": { + "tee": 2, + "memory": 8192, + "cpus": 2, + "storage": 40000 + }, + "payment": { + "native": { + "denomination": "", + "terms": { + "2": "150000000000000000000" + } + } + }, + "capacity": 10, + "metadata": { + "net.oasis.description": "Small compute instance with 2 vCPUs, 8GB RAM, and 40GB storage.\nPerfect for testing and lightweight ROFL applications.\nHosted on Akash decentralized cloud infrastructure.", + "net.oasis.note": "Small instance - ideal for lightweight ROFL applications", + "net.oasis.scheduler.offer": "small" + } + }, + { + "id": "0000000000000001", + "resources": { + "tee": 2, + "memory": 16384, + "cpus": 4, + "storage": 80000 + }, + "payment": { + "native": { + "denomination": "", + "terms": { + "2": "300000000000000000000" + } + } + }, + "capacity": 5, + "metadata": { + "net.oasis.description": "Medium compute instance with 4 vCPUs, 16GB RAM, and 80GB storage.\nGreat for standard ROFL applications with moderate resource needs.\nHosted on Akash decentralized cloud infrastructure.", + "net.oasis.note": "Medium instance - balanced compute and memory", + "net.oasis.scheduler.offer": "medium" + } + }, + { + "id": "0000000000000002", + "resources": { + "tee": 2, + "memory": 28672, + "cpus": 8, + "storage": 180000 + }, + "payment": { + "native": { + "denomination": "", + "terms": { + "2": "600000000000000000000" + } + } + }, + "capacity": 1, + "metadata": { + "net.oasis.description": "Large compute instance with 8 vCPUs, 28GB RAM, and 180GB storage.\nDesigned for resource-intensive ROFL applications.\nHosted on Akash decentralized cloud infrastructure.", + "net.oasis.note": "Large instance - high-performance computing", + "net.oasis.scheduler.offer": "large" + } + } + ] +} diff --git a/examples/rofl/provider-show.in.static b/examples/rofl/provider-show.in.static new file mode 100644 index 00000000..36a00c3b --- /dev/null +++ b/examples/rofl/provider-show.in.static @@ -0,0 +1 @@ +oasis rofl provider show oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3 diff --git a/examples/rofl/provider-show.out.static b/examples/rofl/provider-show.out.static new file mode 100644 index 00000000..1f3537d7 --- /dev/null +++ b/examples/rofl/provider-show.out.static @@ -0,0 +1,41 @@ +Provider: oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3 + +=== Basic Information === +Scheduler App: rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg +Payment Address: oasis1qqw74ezqygseg32e7jq9tl637q7aa4h7qsssmwp3 +Nodes: 1 + 1. mXsy6XlJlEK5vJwEfyqRWZLVN5Ss4QpwI6h124IDjjw= +Stake: 100.0 TEST +Offers: 3 +Instances: 0 +Created At: 2025-11-15T21:40:22Z +Updated At: 2025-11-15T21:40:22Z + +=== Offers === + - small [0000000000000000] + TEE: tdx | Memory: 8192 MiB | vCPUs: 2 | Storage: 39.06 GiB + Capacity: 10 + Note: Small instance - ideal for lightweight ROFL applications + Description: + Small compute instance with 2 vCPUs, 8GB RAM, and 40GB storage. + Perfect for testing and lightweight ROFL applications. + Hosted on Akash decentralized cloud infrastructure. + Payment: monthly: 150.0 TEST + - medium [0000000000000001] + TEE: tdx | Memory: 16384 MiB | vCPUs: 4 | Storage: 78.12 GiB + Capacity: 5 + Note: Medium instance - balanced compute and memory + Description: + Medium compute instance with 4 vCPUs, 16GB RAM, and 80GB storage. + Great for standard ROFL applications with moderate resource needs. + Hosted on Akash decentralized cloud infrastructure. + Payment: monthly: 300.0 TEST + - large [0000000000000002] + TEE: tdx | Memory: 28672 MiB | vCPUs: 8 | Storage: 175.78 GiB + Capacity: 1 + Note: Large instance - high-performance computing + Description: + Large compute instance with 8 vCPUs, 28GB RAM, and 180GB storage. + Designed for resource-intensive ROFL applications. + Hosted on Akash decentralized cloud infrastructure. + Payment: monthly: 600.0 TEST