package cli import ( "bytes" "encoding/base64" "fmt" "github.com/filecoin-project/go-address" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/specs-actors/actors/builtin/paych" "github.com/urfave/cli/v2" types "github.com/filecoin-project/lotus/chain/types" ) var paychCmd = &cli.Command{ Name: "paych", Usage: "Manage payment channels", Subcommands: []*cli.Command{ paychGetCmd, paychListCmd, paychVoucherCmd, paychSettleCmd, paychCloseCmd, }, } var paychGetCmd = &cli.Command{ Name: "get", Usage: "Create a new payment channel or get existing one and add amount to it", ArgsUsage: "[fromAddress toAddress amount]", Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 3 { return ShowHelp(cctx, fmt.Errorf("must pass three arguments: ")) } from, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return ShowHelp(cctx, fmt.Errorf("failed to parse from address: %s", err)) } to, err := address.NewFromString(cctx.Args().Get(1)) if err != nil { return ShowHelp(cctx, fmt.Errorf("failed to parse to address: %s", err)) } amt, err := types.ParseFIL(cctx.Args().Get(2)) if err != nil { return ShowHelp(cctx, fmt.Errorf("parsing amount failed: %s", err)) } api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) // Send a message to chain to create channel / add funds to existing // channel info, err := api.PaychGet(ctx, from, to, types.BigInt(amt)) if err != nil { return err } // Wait for the message to be confirmed chAddr, err := api.PaychGetWaitReady(ctx, info.WaitSentinel) if err != nil { return err } fmt.Fprintln(cctx.App.Writer, chAddr) return nil }, } var paychListCmd = &cli.Command{ Name: "list", Usage: "List all locally registered payment channels", Action: func(cctx *cli.Context) error { api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) chs, err := api.PaychList(ctx) if err != nil { return err } for _, v := range chs { fmt.Fprintln(cctx.App.Writer, v.String()) } return nil }, } var paychSettleCmd = &cli.Command{ Name: "settle", Usage: "Settle a payment channel", ArgsUsage: "[channelAddress]", Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 1 { return fmt.Errorf("must pass payment channel address") } ch, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return fmt.Errorf("failed to parse payment channel address: %s", err) } api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) mcid, err := api.PaychSettle(ctx, ch) if err != nil { return err } mwait, err := api.StateWaitMsg(ctx, mcid, build.MessageConfidence) if err != nil { return nil } if mwait.Receipt.ExitCode != 0 { return fmt.Errorf("settle message execution failed (exit code %d)", mwait.Receipt.ExitCode) } fmt.Fprintf(cctx.App.Writer, "Settled channel %s\n", ch) return nil }, } var paychCloseCmd = &cli.Command{ Name: "collect", Usage: "Collect funds for a payment channel", ArgsUsage: "[channelAddress]", Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 1 { return fmt.Errorf("must pass payment channel address") } ch, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return fmt.Errorf("failed to parse payment channel address: %s", err) } api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) mcid, err := api.PaychCollect(ctx, ch) if err != nil { return err } mwait, err := api.StateWaitMsg(ctx, mcid, build.MessageConfidence) if err != nil { return nil } if mwait.Receipt.ExitCode != 0 { return fmt.Errorf("collect message execution failed (exit code %d)", mwait.Receipt.ExitCode) } fmt.Fprintf(cctx.App.Writer, "Collected funds for channel %s\n", ch) return nil }, } var paychVoucherCmd = &cli.Command{ Name: "voucher", Usage: "Interact with payment channel vouchers", Subcommands: []*cli.Command{ paychVoucherCreateCmd, paychVoucherCheckCmd, paychVoucherAddCmd, paychVoucherListCmd, paychVoucherBestSpendableCmd, paychVoucherSubmitCmd, }, } var paychVoucherCreateCmd = &cli.Command{ Name: "create", Usage: "Create a signed payment channel voucher", ArgsUsage: "[channelAddress amount]", Flags: []cli.Flag{ &cli.IntFlag{ Name: "lane", Value: 0, Usage: "specify payment channel lane to use", }, }, Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 2 { return ShowHelp(cctx, fmt.Errorf("must pass two arguments: ")) } ch, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return err } amt, err := types.BigFromString(cctx.Args().Get(1)) if err != nil { return err } lane := cctx.Int("lane") api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) sv, err := api.PaychVoucherCreate(ctx, ch, amt, uint64(lane)) if err != nil { return err } enc, err := EncodedString(sv) if err != nil { return err } fmt.Fprintln(cctx.App.Writer, enc) return nil }, } var paychVoucherCheckCmd = &cli.Command{ Name: "check", Usage: "Check validity of payment channel voucher", ArgsUsage: "[channelAddress voucher]", Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 2 { return ShowHelp(cctx, fmt.Errorf("must pass payment channel address and voucher to validate")) } ch, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return err } sv, err := types.DecodeSignedVoucher(cctx.Args().Get(1)) if err != nil { return err } api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) if err := api.PaychVoucherCheckValid(ctx, ch, sv); err != nil { return err } fmt.Fprintln(cctx.App.Writer, "voucher is valid") return nil }, } var paychVoucherAddCmd = &cli.Command{ Name: "add", Usage: "Add payment channel voucher to local datastore", ArgsUsage: "[channelAddress voucher]", Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 2 { return ShowHelp(cctx, fmt.Errorf("must pass payment channel address and voucher")) } ch, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return err } sv, err := types.DecodeSignedVoucher(cctx.Args().Get(1)) if err != nil { return err } api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) // TODO: allow passing proof bytes if _, err := api.PaychVoucherAdd(ctx, ch, sv, nil, types.NewInt(0)); err != nil { return err } return nil }, } var paychVoucherListCmd = &cli.Command{ Name: "list", Usage: "List stored vouchers for a given payment channel", ArgsUsage: "[channelAddress]", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "export", Usage: "Print export strings", }, }, Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 1 { return ShowHelp(cctx, fmt.Errorf("must pass payment channel address")) } ch, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return err } api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) vouchers, err := api.PaychVoucherList(ctx, ch) if err != nil { return err } for _, v := range vouchers { if cctx.Bool("export") { enc, err := EncodedString(v) if err != nil { return err } fmt.Fprintf(cctx.App.Writer, "Lane %d, Nonce %d: %s; %s\n", v.Lane, v.Nonce, v.Amount.String(), enc) } else { fmt.Fprintf(cctx.App.Writer, "Lane %d, Nonce %d: %s\n", v.Lane, v.Nonce, v.Amount.String()) } } return nil }, } var paychVoucherBestSpendableCmd = &cli.Command{ Name: "best-spendable", Usage: "Print voucher with highest value that is currently spendable", ArgsUsage: "[channelAddress]", Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 1 { return ShowHelp(cctx, fmt.Errorf("must pass payment channel address")) } ch, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return err } api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) vouchers, err := api.PaychVoucherList(ctx, ch) if err != nil { return err } var best *paych.SignedVoucher for _, v := range vouchers { spendable, err := api.PaychVoucherCheckSpendable(ctx, ch, v, nil, nil) if err != nil { return err } if spendable { if best == nil || v.Amount.GreaterThan(best.Amount) { best = v } } } if best == nil { return fmt.Errorf("No spendable vouchers for that channel") } enc, err := EncodedString(best) if err != nil { return err } fmt.Fprintln(cctx.App.Writer, enc) fmt.Fprintf(cctx.App.Writer, "Amount: %s\n", best.Amount) return nil }, } var paychVoucherSubmitCmd = &cli.Command{ Name: "submit", Usage: "Submit voucher to chain to update payment channel state", ArgsUsage: "[channelAddress voucher]", Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 2 { return ShowHelp(cctx, fmt.Errorf("must pass payment channel address and voucher")) } ch, err := address.NewFromString(cctx.Args().Get(0)) if err != nil { return err } sv, err := types.DecodeSignedVoucher(cctx.Args().Get(1)) if err != nil { return err } api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err } defer closer() ctx := ReqContext(cctx) mcid, err := api.PaychVoucherSubmit(ctx, ch, sv) if err != nil { return err } mwait, err := api.StateWaitMsg(ctx, mcid, build.MessageConfidence) if err != nil { return err } if mwait.Receipt.ExitCode != 0 { return fmt.Errorf("message execution failed (exit code %d)", mwait.Receipt.ExitCode) } fmt.Fprintln(cctx.App.Writer, "channel updated successfully") return nil }, } func EncodedString(sv *paych.SignedVoucher) (string, error) { buf := new(bytes.Buffer) if err := sv.MarshalCBOR(buf); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(buf.Bytes()), nil }