590 lines
14 KiB
Go
590 lines
14 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/filecoin-project/lotus/paychmgr"
|
|
|
|
"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{
|
|
paychAddFundsCmd,
|
|
paychListCmd,
|
|
paychVoucherCmd,
|
|
paychSettleCmd,
|
|
paychCloseCmd,
|
|
},
|
|
}
|
|
|
|
var paychAddFundsCmd = &cli.Command{
|
|
Name: "add-funds",
|
|
Usage: "Add funds to the payment channel between fromAddress and toAddress. Creates the payment channel if it doesn't already exist.",
|
|
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> <to> <available funds>"))
|
|
}
|
|
|
|
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 paychStatusCmd = &cli.Command{
|
|
Name: "status",
|
|
Usage: "Show the status of an outbound payment channel between fromAddress and toAddress",
|
|
ArgsUsage: "[fromAddress toAddress]",
|
|
Action: func(cctx *cli.Context) error {
|
|
if cctx.Args().Len() != 2 {
|
|
return ShowHelp(cctx, fmt.Errorf("must pass two arguments: <from> <to>"))
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer()
|
|
|
|
avail, err := api.PaychAvailableFunds(from, to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if avail.Channel == nil {
|
|
if avail.PendingWaitSentinel != nil {
|
|
fmt.Fprint(cctx.App.Writer, "Creating channel\n")
|
|
fmt.Fprintf(cctx.App.Writer, " From: %s\n", from)
|
|
fmt.Fprintf(cctx.App.Writer, " To: %s\n", to)
|
|
fmt.Fprintf(cctx.App.Writer, " Pending Amt: %d\n", avail.PendingAmt)
|
|
fmt.Fprintf(cctx.App.Writer, " Wait Sentinel: %s\n", avail.PendingWaitSentinel)
|
|
return nil
|
|
}
|
|
fmt.Fprint(cctx.App.Writer, "Channel does not exist\n")
|
|
fmt.Fprintf(cctx.App.Writer, " From: %s\n", from)
|
|
fmt.Fprintf(cctx.App.Writer, " To: %s\n", to)
|
|
return nil
|
|
}
|
|
|
|
if avail.PendingWaitSentinel != nil {
|
|
fmt.Fprint(cctx.App.Writer, "Adding Funds to channel\n")
|
|
} else {
|
|
fmt.Fprint(cctx.App.Writer, "Channel exists\n")
|
|
}
|
|
|
|
nameValues := [][]string{
|
|
{"Channel", avail.Channel.String()},
|
|
{"From", from.String()},
|
|
{"To", to.String()},
|
|
{"Confirmed Amt", fmt.Sprintf("%d", avail.ConfirmedAmt)},
|
|
{"Pending Amt", fmt.Sprintf("%d", avail.PendingAmt)},
|
|
{"Queued Amt", fmt.Sprintf("%d", avail.QueuedAmt)},
|
|
{"Voucher Redeemed Amt", fmt.Sprintf("%d", avail.VoucherReedeemedAmt)},
|
|
}
|
|
if avail.PendingWaitSentinel != nil {
|
|
nameValues = append(nameValues, []string{
|
|
"Add Funds Wait Sentinel",
|
|
avail.PendingWaitSentinel.String(),
|
|
})
|
|
}
|
|
fmt.Fprint(cctx.App.Writer, formatNameValues(nameValues))
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func formatNameValues(nameValues [][]string) string {
|
|
maxLen := 0
|
|
for _, nv := range nameValues {
|
|
if len(nv[0]) > maxLen {
|
|
maxLen = len(nv[0])
|
|
}
|
|
}
|
|
out := make([]string, len(nameValues))
|
|
for i, nv := range nameValues {
|
|
namePad := strings.Repeat(" ", maxLen-len(nv[0]))
|
|
out[i] = " " + nv[0] + ": " + namePad + nv[1]
|
|
}
|
|
return strings.Join(out, "\n") + "\n"
|
|
}
|
|
|
|
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: <channel> <amount>"))
|
|
}
|
|
|
|
ch, err := address.NewFromString(cctx.Args().Get(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
amt, err := types.ParseFIL(cctx.Args().Get(1))
|
|
if err != nil {
|
|
return ShowHelp(cctx, fmt.Errorf("parsing amount failed: %s", err))
|
|
}
|
|
|
|
lane := cctx.Int("lane")
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
v, err := api.PaychVoucherCreate(ctx, ch, types.BigInt(amt), uint64(lane))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v.Voucher == nil {
|
|
return fmt.Errorf("Could not create voucher: insufficient funds in channel, shortfall: %d", v.Shortfall)
|
|
}
|
|
|
|
enc, err := EncodedString(v.Voucher)
|
|
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 voucher as serialized string",
|
|
},
|
|
},
|
|
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 sortVouchers(vouchers) {
|
|
export := cctx.Bool("export")
|
|
err := outputVoucher(cctx.App.Writer, v, export)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var paychVoucherBestSpendableCmd = &cli.Command{
|
|
Name: "best-spendable",
|
|
Usage: "Print vouchers with highest value that is currently spendable for each lane",
|
|
ArgsUsage: "[channelAddress]",
|
|
Flags: []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: "export",
|
|
Usage: "Print voucher as serialized string",
|
|
},
|
|
},
|
|
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)
|
|
|
|
vouchersByLane, err := paychmgr.BestSpendableByLane(ctx, api, ch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var vouchers []*paych.SignedVoucher
|
|
for _, vchr := range vouchersByLane {
|
|
vouchers = append(vouchers, vchr)
|
|
}
|
|
for _, best := range sortVouchers(vouchers) {
|
|
export := cctx.Bool("export")
|
|
err := outputVoucher(cctx.App.Writer, best, export)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func sortVouchers(vouchers []*paych.SignedVoucher) []*paych.SignedVoucher {
|
|
sort.Slice(vouchers, func(i, j int) bool {
|
|
if vouchers[i].Lane == vouchers[j].Lane {
|
|
return vouchers[i].Nonce < vouchers[j].Nonce
|
|
}
|
|
return vouchers[i].Lane < vouchers[j].Lane
|
|
})
|
|
return vouchers
|
|
}
|
|
|
|
func outputVoucher(w io.Writer, v *paych.SignedVoucher, export bool) error {
|
|
var enc string
|
|
if export {
|
|
var err error
|
|
enc, err = EncodedString(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(w, "Lane %d, Nonce %d: %s", v.Lane, v.Nonce, v.Amount.String())
|
|
if export {
|
|
fmt.Fprintf(w, "; %s", enc)
|
|
}
|
|
fmt.Fprintln(w)
|
|
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, nil)
|
|
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
|
|
}
|