1435 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1435 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package cli
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"text/tabwriter"
 | |
| 	"time"
 | |
| 
 | |
| 	tm "github.com/buger/goterm"
 | |
| 	"github.com/docker/go-units"
 | |
| 	"github.com/fatih/color"
 | |
| 	datatransfer "github.com/filecoin-project/go-data-transfer"
 | |
| 	"github.com/filecoin-project/go-fil-markets/retrievalmarket"
 | |
| 	"github.com/ipfs/go-cid"
 | |
| 	"github.com/ipfs/go-cidutil/cidenc"
 | |
| 	"github.com/libp2p/go-libp2p-core/peer"
 | |
| 	"github.com/multiformats/go-multibase"
 | |
| 	"github.com/urfave/cli/v2"
 | |
| 	"golang.org/x/xerrors"
 | |
| 
 | |
| 	"github.com/filecoin-project/go-address"
 | |
| 	"github.com/filecoin-project/go-fil-markets/storagemarket"
 | |
| 	"github.com/filecoin-project/go-multistore"
 | |
| 	"github.com/filecoin-project/go-state-types/abi"
 | |
| 	"github.com/filecoin-project/go-state-types/big"
 | |
| 	"github.com/filecoin-project/specs-actors/actors/builtin/market"
 | |
| 
 | |
| 	"github.com/filecoin-project/lotus/api"
 | |
| 	lapi "github.com/filecoin-project/lotus/api"
 | |
| 	"github.com/filecoin-project/lotus/build"
 | |
| 	"github.com/filecoin-project/lotus/chain/types"
 | |
| 	"github.com/filecoin-project/lotus/lib/tablewriter"
 | |
| )
 | |
| 
 | |
| var CidBaseFlag = cli.StringFlag{
 | |
| 	Name:        "cid-base",
 | |
| 	Hidden:      true,
 | |
| 	Value:       "base32",
 | |
| 	Usage:       "Multibase encoding used for version 1 CIDs in output.",
 | |
| 	DefaultText: "base32",
 | |
| }
 | |
| 
 | |
| // GetCidEncoder returns an encoder using the `cid-base` flag if provided, or
 | |
| // the default (Base32) encoder if not.
 | |
| func GetCidEncoder(cctx *cli.Context) (cidenc.Encoder, error) {
 | |
| 	val := cctx.String("cid-base")
 | |
| 
 | |
| 	e := cidenc.Encoder{Base: multibase.MustNewEncoder(multibase.Base32)}
 | |
| 
 | |
| 	if val != "" {
 | |
| 		var err error
 | |
| 		e.Base, err = multibase.EncoderByName(val)
 | |
| 		if err != nil {
 | |
| 			return e, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return e, nil
 | |
| }
 | |
| 
 | |
| var clientCmd = &cli.Command{
 | |
| 	Name:  "client",
 | |
| 	Usage: "Make deals, store data, retrieve data",
 | |
| 	Subcommands: []*cli.Command{
 | |
| 		WithCategory("storage", clientDealCmd),
 | |
| 		WithCategory("storage", clientQueryAskCmd),
 | |
| 		WithCategory("storage", clientListDeals),
 | |
| 		WithCategory("storage", clientGetDealCmd),
 | |
| 		WithCategory("data", clientImportCmd),
 | |
| 		WithCategory("data", clientDropCmd),
 | |
| 		WithCategory("data", clientLocalCmd),
 | |
| 		WithCategory("retrieval", clientFindCmd),
 | |
| 		WithCategory("retrieval", clientRetrieveCmd),
 | |
| 		WithCategory("util", clientCommPCmd),
 | |
| 		WithCategory("util", clientCarGenCmd),
 | |
| 		WithCategory("util", clientInfoCmd),
 | |
| 		WithCategory("util", clientListTransfers),
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientImportCmd = &cli.Command{
 | |
| 	Name:      "import",
 | |
| 	Usage:     "Import data",
 | |
| 	ArgsUsage: "[inputPath]",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "car",
 | |
| 			Usage: "import from a car file instead of a regular file",
 | |
| 		},
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:    "quiet",
 | |
| 			Aliases: []string{"q"},
 | |
| 			Usage:   "Output root CID only",
 | |
| 		},
 | |
| 		&CidBaseFlag,
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		if cctx.NArg() != 1 {
 | |
| 			return xerrors.New("expected input path as the only arg")
 | |
| 		}
 | |
| 
 | |
| 		absPath, err := filepath.Abs(cctx.Args().First())
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		ref := lapi.FileRef{
 | |
| 			Path:  absPath,
 | |
| 			IsCAR: cctx.Bool("car"),
 | |
| 		}
 | |
| 		c, err := api.ClientImport(ctx, ref)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		encoder, err := GetCidEncoder(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if !cctx.Bool("quiet") {
 | |
| 			fmt.Printf("Import %d, Root ", c.ImportID)
 | |
| 		}
 | |
| 		fmt.Println(encoder.Encode(c.Root))
 | |
| 
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientDropCmd = &cli.Command{
 | |
| 	Name:      "drop",
 | |
| 	Usage:     "Remove import",
 | |
| 	ArgsUsage: "[import ID...]",
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		if !cctx.Args().Present() {
 | |
| 			return xerrors.Errorf("no imports specified")
 | |
| 		}
 | |
| 
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		var ids []multistore.StoreID
 | |
| 		for i, s := range cctx.Args().Slice() {
 | |
| 			id, err := strconv.ParseInt(s, 10, 0)
 | |
| 			if err != nil {
 | |
| 				return xerrors.Errorf("parsing %d-th import ID: %w", i, err)
 | |
| 			}
 | |
| 
 | |
| 			ids = append(ids, multistore.StoreID(id))
 | |
| 		}
 | |
| 
 | |
| 		for _, id := range ids {
 | |
| 			if err := api.ClientRemoveImport(ctx, id); err != nil {
 | |
| 				return xerrors.Errorf("removing import %d: %w", id, err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientCommPCmd = &cli.Command{
 | |
| 	Name:      "commP",
 | |
| 	Usage:     "Calculate the piece-cid (commP) of a CAR file",
 | |
| 	ArgsUsage: "[inputFile]",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&CidBaseFlag,
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		if cctx.Args().Len() != 1 {
 | |
| 			return fmt.Errorf("usage: commP <inputPath>")
 | |
| 		}
 | |
| 
 | |
| 		ret, err := api.ClientCalcCommP(ctx, cctx.Args().Get(0))
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		encoder, err := GetCidEncoder(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		fmt.Println("CID: ", encoder.Encode(ret.Root))
 | |
| 		fmt.Println("Piece size: ", types.SizeStr(types.NewInt(uint64(ret.Size))))
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientCarGenCmd = &cli.Command{
 | |
| 	Name:      "generate-car",
 | |
| 	Usage:     "Generate a car file from input",
 | |
| 	ArgsUsage: "[inputPath outputPath]",
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		if cctx.Args().Len() != 2 {
 | |
| 			return fmt.Errorf("usage: generate-car <inputPath> <outputPath>")
 | |
| 		}
 | |
| 
 | |
| 		ref := lapi.FileRef{
 | |
| 			Path:  cctx.Args().First(),
 | |
| 			IsCAR: false,
 | |
| 		}
 | |
| 
 | |
| 		op := cctx.Args().Get(1)
 | |
| 
 | |
| 		if err = api.ClientGenCar(ctx, ref, op); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientLocalCmd = &cli.Command{
 | |
| 	Name:  "local",
 | |
| 	Usage: "List locally imported data",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&CidBaseFlag,
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		list, err := api.ClientListImports(ctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		encoder, err := GetCidEncoder(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		sort.Slice(list, func(i, j int) bool {
 | |
| 			return list[i].Key < list[j].Key
 | |
| 		})
 | |
| 
 | |
| 		for _, v := range list {
 | |
| 			cidStr := "<nil>"
 | |
| 			if v.Root != nil {
 | |
| 				cidStr = encoder.Encode(*v.Root)
 | |
| 			}
 | |
| 
 | |
| 			fmt.Printf("%d: %s @%s (%s)\n", v.Key, cidStr, v.FilePath, v.Source)
 | |
| 			if v.Err != "" {
 | |
| 				fmt.Printf("\terror: %s\n", v.Err)
 | |
| 			}
 | |
| 		}
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientDealCmd = &cli.Command{
 | |
| 	Name:      "deal",
 | |
| 	Usage:     "Initialize storage deal with a miner",
 | |
| 	ArgsUsage: "[dataCid miner price duration]",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "manual-piece-cid",
 | |
| 			Usage: "manually specify piece commitment for data (dataCid must be to a car file)",
 | |
| 		},
 | |
| 		&cli.Int64Flag{
 | |
| 			Name:  "manual-piece-size",
 | |
| 			Usage: "if manually specifying piece cid, used to specify size (dataCid must be to a car file)",
 | |
| 		},
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "from",
 | |
| 			Usage: "specify address to fund the deal with",
 | |
| 		},
 | |
| 		&cli.Int64Flag{
 | |
| 			Name:  "start-epoch",
 | |
| 			Usage: "specify the epoch that the deal should start at",
 | |
| 			Value: -1,
 | |
| 		},
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "fast-retrieval",
 | |
| 			Usage: "indicates that data should be available for fast retrieval",
 | |
| 			Value: true,
 | |
| 		},
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "verified-deal",
 | |
| 			Usage: "indicate that the deal counts towards verified client total",
 | |
| 			Value: false,
 | |
| 		},
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "provider-collateral",
 | |
| 			Usage: "specify the requested provider collateral the miner should put up",
 | |
| 		},
 | |
| 		&CidBaseFlag,
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		if !cctx.Args().Present() {
 | |
| 			return interactiveDeal(cctx)
 | |
| 		}
 | |
| 
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		if cctx.NArg() != 4 {
 | |
| 			return xerrors.New("expected 4 args: dataCid, miner, price, duration")
 | |
| 		}
 | |
| 
 | |
| 		// [data, miner, price, dur]
 | |
| 
 | |
| 		data, err := cid.Parse(cctx.Args().Get(0))
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		miner, err := address.NewFromString(cctx.Args().Get(1))
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		price, err := types.ParseFIL(cctx.Args().Get(2))
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		dur, err := strconv.ParseInt(cctx.Args().Get(3), 10, 32)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		var provCol big.Int
 | |
| 		if pcs := cctx.String("provider-collateral"); pcs != "" {
 | |
| 			pc, err := big.FromString(pcs)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to parse provider-collateral: %w", err)
 | |
| 			}
 | |
| 			provCol = pc
 | |
| 		}
 | |
| 
 | |
| 		if abi.ChainEpoch(dur) < build.MinDealDuration {
 | |
| 			return xerrors.Errorf("minimum deal duration is %d blocks", build.MinDealDuration)
 | |
| 		}
 | |
| 
 | |
| 		var a address.Address
 | |
| 		if from := cctx.String("from"); from != "" {
 | |
| 			faddr, err := address.NewFromString(from)
 | |
| 			if err != nil {
 | |
| 				return xerrors.Errorf("failed to parse 'from' address: %w", err)
 | |
| 			}
 | |
| 			a = faddr
 | |
| 		} else {
 | |
| 			def, err := api.WalletDefaultAddress(ctx)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			a = def
 | |
| 		}
 | |
| 
 | |
| 		ref := &storagemarket.DataRef{
 | |
| 			TransferType: storagemarket.TTGraphsync,
 | |
| 			Root:         data,
 | |
| 		}
 | |
| 
 | |
| 		if mpc := cctx.String("manual-piece-cid"); mpc != "" {
 | |
| 			c, err := cid.Parse(mpc)
 | |
| 			if err != nil {
 | |
| 				return xerrors.Errorf("failed to parse provided manual piece cid: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			ref.PieceCid = &c
 | |
| 
 | |
| 			psize := cctx.Int64("manual-piece-size")
 | |
| 			if psize == 0 {
 | |
| 				return xerrors.Errorf("must specify piece size when manually setting cid")
 | |
| 			}
 | |
| 
 | |
| 			ref.PieceSize = abi.UnpaddedPieceSize(psize)
 | |
| 
 | |
| 			ref.TransferType = storagemarket.TTManual
 | |
| 		}
 | |
| 
 | |
| 		// Check if the address is a verified client
 | |
| 		dcap, err := api.StateVerifiedClientStatus(ctx, a, types.EmptyTSK)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		isVerified := dcap != nil
 | |
| 
 | |
| 		// If the user has explicitly set the --verified-deal flag
 | |
| 		if cctx.IsSet("verified-deal") {
 | |
| 			// If --verified-deal is true, but the address is not a verified
 | |
| 			// client, return an error
 | |
| 			verifiedDealParam := cctx.Bool("verified-deal")
 | |
| 			if verifiedDealParam && !isVerified {
 | |
| 				return xerrors.Errorf("address %s does not have verified client status", a)
 | |
| 			}
 | |
| 
 | |
| 			// Override the default
 | |
| 			isVerified = verifiedDealParam
 | |
| 		}
 | |
| 
 | |
| 		proposal, err := api.ClientStartDeal(ctx, &lapi.StartDealParams{
 | |
| 			Data:               ref,
 | |
| 			Wallet:             a,
 | |
| 			Miner:              miner,
 | |
| 			EpochPrice:         types.BigInt(price),
 | |
| 			MinBlocksDuration:  uint64(dur),
 | |
| 			DealStartEpoch:     abi.ChainEpoch(cctx.Int64("start-epoch")),
 | |
| 			FastRetrieval:      cctx.Bool("fast-retrieval"),
 | |
| 			VerifiedDeal:       isVerified,
 | |
| 			ProviderCollateral: provCol,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		encoder, err := GetCidEncoder(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		fmt.Println(encoder.Encode(*proposal))
 | |
| 
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| func interactiveDeal(cctx *cli.Context) error {
 | |
| 	api, closer, err := GetFullNodeAPI(cctx)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer closer()
 | |
| 	ctx := ReqContext(cctx)
 | |
| 
 | |
| 	state := "import"
 | |
| 
 | |
| 	var data cid.Cid
 | |
| 	var days int
 | |
| 	var maddr address.Address
 | |
| 	var ask storagemarket.StorageAsk
 | |
| 	var epochPrice big.Int
 | |
| 	var epochs abi.ChainEpoch
 | |
| 
 | |
| 	var a address.Address
 | |
| 	if from := cctx.String("from"); from != "" {
 | |
| 		faddr, err := address.NewFromString(from)
 | |
| 		if err != nil {
 | |
| 			return xerrors.Errorf("failed to parse 'from' address: %w", err)
 | |
| 		}
 | |
| 		a = faddr
 | |
| 	} else {
 | |
| 		def, err := api.WalletDefaultAddress(ctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		a = def
 | |
| 	}
 | |
| 
 | |
| 	printErr := func(err error) {
 | |
| 		fmt.Printf("%s %s\n", color.RedString("Error:"), err.Error())
 | |
| 	}
 | |
| 
 | |
| 	for {
 | |
| 		// TODO: better exit handling
 | |
| 		if err := ctx.Err(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		switch state {
 | |
| 		case "import":
 | |
| 			fmt.Print("Data CID (from " + color.YellowString("lotus client import") + "): ")
 | |
| 
 | |
| 			var cidStr string
 | |
| 			_, err := fmt.Scan(&cidStr)
 | |
| 			if err != nil {
 | |
| 				printErr(xerrors.Errorf("reading cid string: %w", err))
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			data, err = cid.Parse(cidStr)
 | |
| 			if err != nil {
 | |
| 				printErr(xerrors.Errorf("parsing cid string: %w", err))
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			state = "duration"
 | |
| 		case "duration":
 | |
| 			fmt.Print("Deal duration (days): ")
 | |
| 
 | |
| 			_, err := fmt.Scan(&days)
 | |
| 			if err != nil {
 | |
| 				printErr(xerrors.Errorf("parsing duration: %w", err))
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			state = "miner"
 | |
| 		case "miner":
 | |
| 			fmt.Print("Miner Address (t0..): ")
 | |
| 			var maddrStr string
 | |
| 
 | |
| 			_, err := fmt.Scan(&maddrStr)
 | |
| 			if err != nil {
 | |
| 				printErr(xerrors.Errorf("reading miner address: %w", err))
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			maddr, err = address.NewFromString(maddrStr)
 | |
| 			if err != nil {
 | |
| 				printErr(xerrors.Errorf("parsing miner address: %w", err))
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			state = "query"
 | |
| 		case "query":
 | |
| 			color.Blue(".. querying miner ask")
 | |
| 
 | |
| 			mi, err := api.StateMinerInfo(ctx, maddr, types.EmptyTSK)
 | |
| 			if err != nil {
 | |
| 				printErr(xerrors.Errorf("failed to get peerID for miner: %w", err))
 | |
| 				state = "miner"
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			a, err := api.ClientQueryAsk(ctx, *mi.PeerId, maddr)
 | |
| 			if err != nil {
 | |
| 				printErr(xerrors.Errorf("failed to query ask: %w", err))
 | |
| 				state = "miner"
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			ask = *a.Ask
 | |
| 
 | |
| 			// TODO: run more validation
 | |
| 			state = "confirm"
 | |
| 		case "confirm":
 | |
| 			fromBal, err := api.WalletBalance(ctx, a)
 | |
| 			if err != nil {
 | |
| 				return xerrors.Errorf("checking from address balance: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			color.Blue(".. calculating data size\n")
 | |
| 			ds, err := api.ClientDealSize(ctx, data)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			dur := 24 * time.Hour * time.Duration(days)
 | |
| 
 | |
| 			epochs = abi.ChainEpoch(dur / (time.Duration(build.BlockDelaySecs) * time.Second))
 | |
| 			// TODO: do some more or epochs math (round to miner PP, deal start buffer)
 | |
| 
 | |
| 			gib := types.NewInt(1 << 30)
 | |
| 
 | |
| 			// TODO: price is based on PaddedPieceSize, right?
 | |
| 			epochPrice = types.BigDiv(types.BigMul(ask.Price, types.NewInt(uint64(ds.PieceSize))), gib)
 | |
| 			totalPrice := types.BigMul(epochPrice, types.NewInt(uint64(epochs)))
 | |
| 
 | |
| 			fmt.Printf("-----\n")
 | |
| 			fmt.Printf("Proposing from %s\n", a)
 | |
| 			fmt.Printf("\tBalance: %s\n", types.FIL(fromBal))
 | |
| 			fmt.Printf("\n")
 | |
| 			fmt.Printf("Piece size: %s (Payload size: %s)\n", units.BytesSize(float64(ds.PieceSize)), units.BytesSize(float64(ds.PayloadSize)))
 | |
| 			fmt.Printf("Duration: %s\n", dur)
 | |
| 			fmt.Printf("Total price: ~%s (%s per epoch)\n", types.FIL(totalPrice), types.FIL(epochPrice))
 | |
| 
 | |
| 			state = "accept"
 | |
| 		case "accept":
 | |
| 			fmt.Print("\nAccept (yes/no): ")
 | |
| 
 | |
| 			var yn string
 | |
| 			_, err := fmt.Scan(&yn)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			if yn == "no" {
 | |
| 				return nil
 | |
| 			}
 | |
| 
 | |
| 			if yn != "yes" {
 | |
| 				fmt.Println("Type in full 'yes' or 'no'")
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			state = "execute"
 | |
| 		case "execute":
 | |
| 			color.Blue(".. executing")
 | |
| 			proposal, err := api.ClientStartDeal(ctx, &lapi.StartDealParams{
 | |
| 				Data: &storagemarket.DataRef{
 | |
| 					TransferType: storagemarket.TTGraphsync,
 | |
| 					Root:         data,
 | |
| 				},
 | |
| 				Wallet:            a,
 | |
| 				Miner:             maddr,
 | |
| 				EpochPrice:        epochPrice,
 | |
| 				MinBlocksDuration: uint64(epochs),
 | |
| 				DealStartEpoch:    abi.ChainEpoch(cctx.Int64("start-epoch")),
 | |
| 				FastRetrieval:     cctx.Bool("fast-retrieval"),
 | |
| 				VerifiedDeal:      false, // TODO: Allow setting
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			encoder, err := GetCidEncoder(cctx)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			fmt.Println("\nDeal CID:", color.GreenString(encoder.Encode(*proposal)))
 | |
| 			return nil
 | |
| 		default:
 | |
| 			return xerrors.Errorf("unknown state: %s", state)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var clientFindCmd = &cli.Command{
 | |
| 	Name:      "find",
 | |
| 	Usage:     "Find data in the network",
 | |
| 	ArgsUsage: "[dataCid]",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "pieceCid",
 | |
| 			Usage: "require data to be retrieved from a specific Piece CID",
 | |
| 		},
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		if !cctx.Args().Present() {
 | |
| 			fmt.Println("Usage: find [CID]")
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		file, err := cid.Parse(cctx.Args().First())
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		// Check if we already have this data locally
 | |
| 
 | |
| 		has, err := api.ClientHasLocal(ctx, file)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if has {
 | |
| 			fmt.Println("LOCAL")
 | |
| 		}
 | |
| 
 | |
| 		var pieceCid *cid.Cid
 | |
| 		if cctx.String("pieceCid") != "" {
 | |
| 			parsed, err := cid.Parse(cctx.String("pieceCid"))
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			pieceCid = &parsed
 | |
| 		}
 | |
| 
 | |
| 		offers, err := api.ClientFindData(ctx, file, pieceCid)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		for _, offer := range offers {
 | |
| 			if offer.Err != "" {
 | |
| 				fmt.Printf("ERR %s@%s: %s\n", offer.Miner, offer.MinerPeer.ID, offer.Err)
 | |
| 				continue
 | |
| 			}
 | |
| 			fmt.Printf("RETRIEVAL %s@%s-%s-%s\n", offer.Miner, offer.MinerPeer.ID, types.FIL(offer.MinPrice), types.SizeStr(types.NewInt(offer.Size)))
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| const DefaultMaxRetrievePrice = 1
 | |
| 
 | |
| var clientRetrieveCmd = &cli.Command{
 | |
| 	Name:      "retrieve",
 | |
| 	Usage:     "Retrieve data from network",
 | |
| 	ArgsUsage: "[dataCid outputPath]",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "from",
 | |
| 			Usage: "address to send transactions from",
 | |
| 		},
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "car",
 | |
| 			Usage: "export to a car file instead of a regular file",
 | |
| 		},
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "miner",
 | |
| 			Usage: "miner address for retrieval, if not present it'll use local discovery",
 | |
| 		},
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "maxPrice",
 | |
| 			Usage: fmt.Sprintf("maximum price the client is willing to consider (default: %d FIL)", DefaultMaxRetrievePrice),
 | |
| 		},
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "pieceCid",
 | |
| 			Usage: "require data to be retrieved from a specific Piece CID",
 | |
| 		},
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		if cctx.NArg() != 2 {
 | |
| 			return ShowHelp(cctx, fmt.Errorf("incorrect number of arguments"))
 | |
| 		}
 | |
| 
 | |
| 		fapi, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		var payer address.Address
 | |
| 		if cctx.String("from") != "" {
 | |
| 			payer, err = address.NewFromString(cctx.String("from"))
 | |
| 		} else {
 | |
| 			payer, err = fapi.WalletDefaultAddress(ctx)
 | |
| 		}
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		file, err := cid.Parse(cctx.Args().Get(0))
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		// Check if we already have this data locally
 | |
| 
 | |
| 		/*has, err := api.ClientHasLocal(ctx, file)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if has {
 | |
| 			fmt.Println("Success: Already in local storage")
 | |
| 			return nil
 | |
| 		}*/ // TODO: fix
 | |
| 
 | |
| 		var pieceCid *cid.Cid
 | |
| 		if cctx.String("pieceCid") != "" {
 | |
| 			parsed, err := cid.Parse(cctx.String("pieceCid"))
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			pieceCid = &parsed
 | |
| 		}
 | |
| 
 | |
| 		var offer api.QueryOffer
 | |
| 		minerStrAddr := cctx.String("miner")
 | |
| 		if minerStrAddr == "" { // Local discovery
 | |
| 			offers, err := fapi.ClientFindData(ctx, file, pieceCid)
 | |
| 
 | |
| 			var cleaned []api.QueryOffer
 | |
| 			// filter out offers that errored
 | |
| 			for _, o := range offers {
 | |
| 				if o.Err == "" {
 | |
| 					cleaned = append(cleaned, o)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			offers = cleaned
 | |
| 
 | |
| 			// sort by price low to high
 | |
| 			sort.Slice(offers, func(i, j int) bool {
 | |
| 				return offers[i].MinPrice.LessThan(offers[j].MinPrice)
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			// TODO: parse offer strings from `client find`, make this smarter
 | |
| 			if len(offers) < 1 {
 | |
| 				fmt.Println("Failed to find file")
 | |
| 				return nil
 | |
| 			}
 | |
| 			offer = offers[0]
 | |
| 		} else { // Directed retrieval
 | |
| 			minerAddr, err := address.NewFromString(minerStrAddr)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			offer, err = fapi.ClientMinerQueryOffer(ctx, minerAddr, file, pieceCid)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 		if offer.Err != "" {
 | |
| 			return fmt.Errorf("The received offer errored: %s", offer.Err)
 | |
| 		}
 | |
| 
 | |
| 		maxPrice := types.FromFil(DefaultMaxRetrievePrice)
 | |
| 
 | |
| 		if cctx.String("maxPrice") != "" {
 | |
| 			maxPriceFil, err := types.ParseFIL(cctx.String("maxPrice"))
 | |
| 			if err != nil {
 | |
| 				return xerrors.Errorf("parsing maxPrice: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			maxPrice = types.BigInt(maxPriceFil)
 | |
| 		}
 | |
| 
 | |
| 		if offer.MinPrice.GreaterThan(maxPrice) {
 | |
| 			return xerrors.Errorf("failed to find offer satisfying maxPrice: %s", maxPrice)
 | |
| 		}
 | |
| 
 | |
| 		ref := &lapi.FileRef{
 | |
| 			Path:  cctx.Args().Get(1),
 | |
| 			IsCAR: cctx.Bool("car"),
 | |
| 		}
 | |
| 		updates, err := fapi.ClientRetrieveWithEvents(ctx, offer.Order(payer), ref)
 | |
| 		if err != nil {
 | |
| 			return xerrors.Errorf("error setting up retrieval: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		for {
 | |
| 			select {
 | |
| 			case evt, ok := <-updates:
 | |
| 				if ok {
 | |
| 					fmt.Printf("> Recv: %s, Paid %s, %s (%s)\n",
 | |
| 						types.SizeStr(types.NewInt(evt.BytesReceived)),
 | |
| 						types.FIL(evt.FundsSpent),
 | |
| 						retrievalmarket.ClientEvents[evt.Event],
 | |
| 						retrievalmarket.DealStatuses[evt.Status],
 | |
| 					)
 | |
| 				} else {
 | |
| 					fmt.Println("Success")
 | |
| 					return nil
 | |
| 				}
 | |
| 
 | |
| 				if evt.Err != "" {
 | |
| 					return xerrors.Errorf("retrieval failed: %s", evt.Err)
 | |
| 				}
 | |
| 			case <-ctx.Done():
 | |
| 				return xerrors.Errorf("retrieval timed out")
 | |
| 			}
 | |
| 		}
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientQueryAskCmd = &cli.Command{
 | |
| 	Name:      "query-ask",
 | |
| 	Usage:     "Find a miners ask",
 | |
| 	ArgsUsage: "[minerAddress]",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "peerid",
 | |
| 			Usage: "specify peer ID of node to make query against",
 | |
| 		},
 | |
| 		&cli.Int64Flag{
 | |
| 			Name:  "size",
 | |
| 			Usage: "data size in bytes",
 | |
| 		},
 | |
| 		&cli.Int64Flag{
 | |
| 			Name:  "duration",
 | |
| 			Usage: "deal duration",
 | |
| 		},
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		if cctx.NArg() != 1 {
 | |
| 			fmt.Println("Usage: query-ask [minerAddress]")
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		maddr, err := address.NewFromString(cctx.Args().First())
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		var pid peer.ID
 | |
| 		if pidstr := cctx.String("peerid"); pidstr != "" {
 | |
| 			p, err := peer.Decode(pidstr)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			pid = p
 | |
| 		} else {
 | |
| 			mi, err := api.StateMinerInfo(ctx, maddr, types.EmptyTSK)
 | |
| 			if err != nil {
 | |
| 				return xerrors.Errorf("failed to get peerID for miner: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			if *mi.PeerId == peer.ID("SETME") {
 | |
| 				return fmt.Errorf("the miner hasn't initialized yet")
 | |
| 			}
 | |
| 
 | |
| 			pid = *mi.PeerId
 | |
| 		}
 | |
| 
 | |
| 		ask, err := api.ClientQueryAsk(ctx, pid, maddr)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		fmt.Printf("Ask: %s\n", maddr)
 | |
| 		fmt.Printf("Price per GiB: %s\n", types.FIL(ask.Ask.Price))
 | |
| 		fmt.Printf("Verified Price per GiB: %s\n", types.FIL(ask.Ask.VerifiedPrice))
 | |
| 		fmt.Printf("Max Piece size: %s\n", types.SizeStr(types.NewInt(uint64(ask.Ask.MaxPieceSize))))
 | |
| 
 | |
| 		size := cctx.Int64("size")
 | |
| 		if size == 0 {
 | |
| 			return nil
 | |
| 		}
 | |
| 		perEpoch := types.BigDiv(types.BigMul(ask.Ask.Price, types.NewInt(uint64(size))), types.NewInt(1<<30))
 | |
| 		fmt.Printf("Price per Block: %s\n", types.FIL(perEpoch))
 | |
| 
 | |
| 		duration := cctx.Int64("duration")
 | |
| 		if duration == 0 {
 | |
| 			return nil
 | |
| 		}
 | |
| 		fmt.Printf("Total Price: %s\n", types.FIL(types.BigMul(perEpoch, types.NewInt(uint64(duration)))))
 | |
| 
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientListDeals = &cli.Command{
 | |
| 	Name:  "list-deals",
 | |
| 	Usage: "List storage market deals",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:    "verbose",
 | |
| 			Aliases: []string{"v"},
 | |
| 			Usage:   "print verbose deal details",
 | |
| 		},
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "color",
 | |
| 			Usage: "use color in display output",
 | |
| 			Value: true,
 | |
| 		},
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "watch",
 | |
| 			Usage: "watch deal updates in real-time, rather than a one time list",
 | |
| 		},
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		verbose := cctx.Bool("verbose")
 | |
| 		color := cctx.Bool("color")
 | |
| 		watch := cctx.Bool("watch")
 | |
| 
 | |
| 		localDeals, err := api.ClientListDeals(ctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if watch {
 | |
| 			updates, err := api.ClientGetDealUpdates(ctx)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			for {
 | |
| 				tm.Clear()
 | |
| 				tm.MoveCursor(1, 1)
 | |
| 
 | |
| 				err = outputStorageDeals(ctx, tm.Screen, api, localDeals, verbose, color)
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				tm.Flush()
 | |
| 
 | |
| 				select {
 | |
| 				case <-ctx.Done():
 | |
| 					return nil
 | |
| 				case updated := <-updates:
 | |
| 					var found bool
 | |
| 					for i, existing := range localDeals {
 | |
| 						if existing.ProposalCid.Equals(updated.ProposalCid) {
 | |
| 							localDeals[i] = updated
 | |
| 							found = true
 | |
| 							break
 | |
| 						}
 | |
| 					}
 | |
| 					if !found {
 | |
| 						localDeals = append(localDeals, updated)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return outputStorageDeals(ctx, os.Stdout, api, localDeals, cctx.Bool("verbose"), cctx.Bool("color"))
 | |
| 	},
 | |
| }
 | |
| 
 | |
| func dealFromDealInfo(ctx context.Context, full api.FullNode, head *types.TipSet, v api.DealInfo) deal {
 | |
| 	if v.DealID == 0 {
 | |
| 		return deal{
 | |
| 			LocalDeal: v,
 | |
| 			OnChainDealState: market.DealState{
 | |
| 				SectorStartEpoch: -1,
 | |
| 				LastUpdatedEpoch: -1,
 | |
| 				SlashEpoch:       -1,
 | |
| 			},
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	onChain, err := full.StateMarketStorageDeal(ctx, v.DealID, head.Key())
 | |
| 	if err != nil {
 | |
| 		return deal{LocalDeal: v}
 | |
| 	}
 | |
| 
 | |
| 	return deal{
 | |
| 		LocalDeal:        v,
 | |
| 		OnChainDealState: onChain.State,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func outputStorageDeals(ctx context.Context, out io.Writer, full api.FullNode, localDeals []api.DealInfo, verbose bool, color bool) error {
 | |
| 	sort.Slice(localDeals, func(i, j int) bool {
 | |
| 		return localDeals[i].CreationTime.Before(localDeals[j].CreationTime)
 | |
| 	})
 | |
| 
 | |
| 	head, err := full.ChainHead(ctx)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	var deals []deal
 | |
| 	for _, localDeal := range localDeals {
 | |
| 		deals = append(deals, dealFromDealInfo(ctx, full, head, localDeal))
 | |
| 	}
 | |
| 
 | |
| 	if verbose {
 | |
| 		w := tabwriter.NewWriter(out, 2, 4, 2, ' ', 0)
 | |
| 		fmt.Fprintf(w, "Created\tDealCid\tDealId\tProvider\tState\tOn Chain?\tSlashed?\tPieceCID\tSize\tPrice\tDuration\tMessage\n")
 | |
| 		for _, d := range deals {
 | |
| 			onChain := "N"
 | |
| 			if d.OnChainDealState.SectorStartEpoch != -1 {
 | |
| 				onChain = fmt.Sprintf("Y (epoch %d)", d.OnChainDealState.SectorStartEpoch)
 | |
| 			}
 | |
| 
 | |
| 			slashed := "N"
 | |
| 			if d.OnChainDealState.SlashEpoch != -1 {
 | |
| 				slashed = fmt.Sprintf("Y (epoch %d)", d.OnChainDealState.SlashEpoch)
 | |
| 			}
 | |
| 
 | |
| 			price := types.FIL(types.BigMul(d.LocalDeal.PricePerEpoch, types.NewInt(d.LocalDeal.Duration)))
 | |
| 			fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s\n", d.LocalDeal.CreationTime.Format(time.Stamp), d.LocalDeal.ProposalCid, d.LocalDeal.DealID, d.LocalDeal.Provider, dealStateString(color, d.LocalDeal.State), onChain, slashed, d.LocalDeal.PieceCID, types.SizeStr(types.NewInt(d.LocalDeal.Size)), price, d.LocalDeal.Duration, d.LocalDeal.Message)
 | |
| 		}
 | |
| 		return w.Flush()
 | |
| 	}
 | |
| 
 | |
| 	w := tablewriter.New(tablewriter.Col("DealCid"),
 | |
| 		tablewriter.Col("DealId"),
 | |
| 		tablewriter.Col("Provider"),
 | |
| 		tablewriter.Col("State"),
 | |
| 		tablewriter.Col("On Chain?"),
 | |
| 		tablewriter.Col("Slashed?"),
 | |
| 		tablewriter.Col("PieceCID"),
 | |
| 		tablewriter.Col("Size"),
 | |
| 		tablewriter.Col("Price"),
 | |
| 		tablewriter.Col("Duration"),
 | |
| 		tablewriter.NewLineCol("Message"))
 | |
| 
 | |
| 	for _, d := range deals {
 | |
| 		propcid := ellipsis(d.LocalDeal.ProposalCid.String(), 8)
 | |
| 
 | |
| 		onChain := "N"
 | |
| 		if d.OnChainDealState.SectorStartEpoch != -1 {
 | |
| 			onChain = fmt.Sprintf("Y (epoch %d)", d.OnChainDealState.SectorStartEpoch)
 | |
| 		}
 | |
| 
 | |
| 		slashed := "N"
 | |
| 		if d.OnChainDealState.SlashEpoch != -1 {
 | |
| 			slashed = fmt.Sprintf("Y (epoch %d)", d.OnChainDealState.SlashEpoch)
 | |
| 		}
 | |
| 
 | |
| 		piece := ellipsis(d.LocalDeal.PieceCID.String(), 8)
 | |
| 
 | |
| 		price := types.FIL(types.BigMul(d.LocalDeal.PricePerEpoch, types.NewInt(d.LocalDeal.Duration)))
 | |
| 
 | |
| 		w.Write(map[string]interface{}{
 | |
| 			"DealCid":   propcid,
 | |
| 			"DealId":    d.LocalDeal.DealID,
 | |
| 			"Provider":  d.LocalDeal.Provider,
 | |
| 			"State":     dealStateString(color, d.LocalDeal.State),
 | |
| 			"On Chain?": onChain,
 | |
| 			"Slashed?":  slashed,
 | |
| 			"PieceCID":  piece,
 | |
| 			"Size":      types.SizeStr(types.NewInt(d.LocalDeal.Size)),
 | |
| 			"Price":     price,
 | |
| 			"Duration":  d.LocalDeal.Duration,
 | |
| 			"Message":   d.LocalDeal.Message,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	return w.Flush(out)
 | |
| }
 | |
| 
 | |
| func dealStateString(c bool, state storagemarket.StorageDealStatus) string {
 | |
| 	s := storagemarket.DealStates[state]
 | |
| 	if !c {
 | |
| 		return s
 | |
| 	}
 | |
| 
 | |
| 	switch state {
 | |
| 	case storagemarket.StorageDealError, storagemarket.StorageDealExpired:
 | |
| 		return color.RedString(s)
 | |
| 	case storagemarket.StorageDealActive:
 | |
| 		return color.GreenString(s)
 | |
| 	default:
 | |
| 		return s
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type deal struct {
 | |
| 	LocalDeal        lapi.DealInfo
 | |
| 	OnChainDealState market.DealState
 | |
| }
 | |
| 
 | |
| var clientGetDealCmd = &cli.Command{
 | |
| 	Name:  "get-deal",
 | |
| 	Usage: "Print detailed deal information",
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		if !cctx.Args().Present() {
 | |
| 			return cli.ShowCommandHelp(cctx, cctx.Command.Name)
 | |
| 		}
 | |
| 
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		propcid, err := cid.Decode(cctx.Args().First())
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		di, err := api.ClientGetDealInfo(ctx, propcid)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		out := map[string]interface{}{
 | |
| 			"DealInfo: ": di,
 | |
| 		}
 | |
| 
 | |
| 		if di.DealID != 0 {
 | |
| 			onChain, err := api.StateMarketStorageDeal(ctx, di.DealID, types.EmptyTSK)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			out["OnChain"] = onChain
 | |
| 		}
 | |
| 
 | |
| 		b, err := json.MarshalIndent(out, "", "  ")
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		fmt.Println(string(b))
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientInfoCmd = &cli.Command{
 | |
| 	Name:  "info",
 | |
| 	Usage: "Print storage market client information",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&cli.StringFlag{
 | |
| 			Name:  "client",
 | |
| 			Usage: "specify storage client address",
 | |
| 		},
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		var addr address.Address
 | |
| 		if clientFlag := cctx.String("client"); clientFlag != "" {
 | |
| 			ca, err := address.NewFromString("client")
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			addr = ca
 | |
| 		} else {
 | |
| 			def, err := api.WalletDefaultAddress(ctx)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			addr = def
 | |
| 		}
 | |
| 
 | |
| 		balance, err := api.StateMarketBalance(ctx, addr, types.EmptyTSK)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		fmt.Printf("Client Market Info:\n")
 | |
| 
 | |
| 		fmt.Printf("Locked Funds:\t%s\n", types.FIL(balance.Locked))
 | |
| 		fmt.Printf("Escrowed Funds:\t%s\n", types.FIL(balance.Escrow))
 | |
| 
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var clientListTransfers = &cli.Command{
 | |
| 	Name:  "list-transfers",
 | |
| 	Usage: "List ongoing data transfers for deals",
 | |
| 	Flags: []cli.Flag{
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "color",
 | |
| 			Usage: "use color in display output",
 | |
| 			Value: true,
 | |
| 		},
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "completed",
 | |
| 			Usage: "show completed data transfers",
 | |
| 		},
 | |
| 		&cli.BoolFlag{
 | |
| 			Name:  "watch",
 | |
| 			Usage: "watch deal updates in real-time, rather than a one time list",
 | |
| 		},
 | |
| 	},
 | |
| 	Action: func(cctx *cli.Context) error {
 | |
| 		api, closer, err := GetFullNodeAPI(cctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer closer()
 | |
| 		ctx := ReqContext(cctx)
 | |
| 
 | |
| 		channels, err := api.ClientListDataTransfers(ctx)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		completed := cctx.Bool("completed")
 | |
| 		color := cctx.Bool("color")
 | |
| 		watch := cctx.Bool("watch")
 | |
| 
 | |
| 		if watch {
 | |
| 			channelUpdates, err := api.ClientDataTransferUpdates(ctx)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			for {
 | |
| 				tm.Clear() // Clear current screen
 | |
| 
 | |
| 				tm.MoveCursor(1, 1)
 | |
| 
 | |
| 				OutputDataTransferChannels(tm.Screen, channels, completed, color)
 | |
| 
 | |
| 				tm.Flush()
 | |
| 
 | |
| 				select {
 | |
| 				case <-ctx.Done():
 | |
| 					return nil
 | |
| 				case channelUpdate := <-channelUpdates:
 | |
| 					var found bool
 | |
| 					for i, existing := range channels {
 | |
| 						if existing.TransferID == channelUpdate.TransferID &&
 | |
| 							existing.OtherPeer == channelUpdate.OtherPeer &&
 | |
| 							existing.IsSender == channelUpdate.IsSender &&
 | |
| 							existing.IsInitiator == channelUpdate.IsInitiator {
 | |
| 							channels[i] = channelUpdate
 | |
| 							found = true
 | |
| 							break
 | |
| 						}
 | |
| 					}
 | |
| 					if !found {
 | |
| 						channels = append(channels, channelUpdate)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		OutputDataTransferChannels(os.Stdout, channels, completed, color)
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| // OutputDataTransferChannels generates table output for a list of channels
 | |
| func OutputDataTransferChannels(out io.Writer, channels []lapi.DataTransferChannel, completed bool, color bool) {
 | |
| 	sort.Slice(channels, func(i, j int) bool {
 | |
| 		return channels[i].TransferID < channels[j].TransferID
 | |
| 	})
 | |
| 
 | |
| 	var receivingChannels, sendingChannels []lapi.DataTransferChannel
 | |
| 	for _, channel := range channels {
 | |
| 		if !completed && channel.Status == datatransfer.Completed {
 | |
| 			continue
 | |
| 		}
 | |
| 		if channel.IsSender {
 | |
| 			sendingChannels = append(sendingChannels, channel)
 | |
| 		} else {
 | |
| 			receivingChannels = append(receivingChannels, channel)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	fmt.Fprintf(out, "Sending Channels\n\n")
 | |
| 	w := tablewriter.New(tablewriter.Col("ID"),
 | |
| 		tablewriter.Col("Status"),
 | |
| 		tablewriter.Col("Sending To"),
 | |
| 		tablewriter.Col("Root Cid"),
 | |
| 		tablewriter.Col("Initiated?"),
 | |
| 		tablewriter.Col("Transferred"),
 | |
| 		tablewriter.Col("Voucher"),
 | |
| 		tablewriter.NewLineCol("Message"))
 | |
| 	for _, channel := range sendingChannels {
 | |
| 		w.Write(toChannelOutput(color, "Sending To", channel))
 | |
| 	}
 | |
| 	w.Flush(out) //nolint:errcheck
 | |
| 
 | |
| 	fmt.Fprintf(out, "\nReceiving Channels\n\n")
 | |
| 	w = tablewriter.New(tablewriter.Col("ID"),
 | |
| 		tablewriter.Col("Status"),
 | |
| 		tablewriter.Col("Receiving From"),
 | |
| 		tablewriter.Col("Root Cid"),
 | |
| 		tablewriter.Col("Initiated?"),
 | |
| 		tablewriter.Col("Transferred"),
 | |
| 		tablewriter.Col("Voucher"),
 | |
| 		tablewriter.NewLineCol("Message"))
 | |
| 	for _, channel := range receivingChannels {
 | |
| 		w.Write(toChannelOutput(color, "Receiving From", channel))
 | |
| 	}
 | |
| 	w.Flush(out) //nolint:errcheck
 | |
| }
 | |
| 
 | |
| func channelStatusString(useColor bool, status datatransfer.Status) string {
 | |
| 	s := datatransfer.Statuses[status]
 | |
| 	if !useColor {
 | |
| 		return s
 | |
| 	}
 | |
| 
 | |
| 	switch status {
 | |
| 	case datatransfer.Failed, datatransfer.Cancelled:
 | |
| 		return color.RedString(s)
 | |
| 	case datatransfer.Completed:
 | |
| 		return color.GreenString(s)
 | |
| 	default:
 | |
| 		return s
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func toChannelOutput(useColor bool, otherPartyColumn string, channel lapi.DataTransferChannel) map[string]interface{} {
 | |
| 	rootCid := ellipsis(channel.BaseCID.String(), 8)
 | |
| 	otherParty := ellipsis(channel.OtherPeer.String(), 8)
 | |
| 
 | |
| 	initiated := "N"
 | |
| 	if channel.IsInitiator {
 | |
| 		initiated = "Y"
 | |
| 	}
 | |
| 
 | |
| 	voucher := channel.Voucher
 | |
| 	if len(voucher) > 40 {
 | |
| 		voucher = ellipsis(voucher, 37)
 | |
| 	}
 | |
| 
 | |
| 	return map[string]interface{}{
 | |
| 		"ID":             channel.TransferID,
 | |
| 		"Status":         channelStatusString(useColor, channel.Status),
 | |
| 		otherPartyColumn: otherParty,
 | |
| 		"Root Cid":       rootCid,
 | |
| 		"Initiated?":     initiated,
 | |
| 		"Transferred":    channel.Transferred,
 | |
| 		"Voucher":        voucher,
 | |
| 		"Message":        channel.Message,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func ellipsis(s string, length int) string {
 | |
| 	if length > 0 && len(s) > length {
 | |
| 		return "..." + s[len(s)-length:]
 | |
| 	}
 | |
| 	return s
 | |
| }
 |