diff --git a/cli/paych.go b/cli/paych.go index ce92af0bc0b..26bb7612709 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "sort" + "strings" "github.com/filecoin-project/lotus/paychmgr" @@ -79,6 +80,92 @@ var paychGetCmd = &cli.Command{ }, } +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, 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", diff --git a/cli/paych_test.go b/cli/paych_test.go index 31d41e36ab7..f03fa7b41a1 100644 --- a/cli/paych_test.go +++ b/cli/paych_test.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/specs-actors/actors/abi/big" saminer "github.com/filecoin-project/specs-actors/actors/builtin/miner" "github.com/filecoin-project/specs-actors/actors/builtin/power" @@ -53,7 +55,7 @@ func TestPaymentChannels(t *testing.T) { ctx := context.Background() nodes, addrs := startTwoNodesOneMiner(ctx, t, blocktime) paymentCreator := nodes[0] - paymentReceiver := nodes[0] + paymentReceiver := nodes[1] creatorAddr := addrs[0] receiverAddr := addrs[1] @@ -99,6 +101,83 @@ type voucherSpec struct { lane int } +// TestPaymentChannelStatus tests the payment channel status CLI command +func TestPaymentChannelStatus(t *testing.T) { + _ = os.Setenv("BELLMAN_NO_GPU", "1") + + blocktime := 5 * time.Millisecond + ctx := context.Background() + nodes, addrs := startTwoNodesOneMiner(ctx, t, blocktime) + paymentCreator := nodes[0] + creatorAddr := addrs[0] + receiverAddr := addrs[1] + + // Create mock CLI + mockCLI := newMockCLI(t) + creatorCLI := mockCLI.client(paymentCreator.ListenAddr) + + cmd := []string{creatorAddr.String(), receiverAddr.String()} + out := creatorCLI.runCmd(paychStatusCmd, cmd) + fmt.Println(out) + noChannelState := "Channel does not exist" + require.Regexp(t, regexp.MustCompile(noChannelState), out) + + channelAmt := uint64(100) + create := make(chan string) + go func() { + // creator: paych get + cmd = []string{creatorAddr.String(), receiverAddr.String(), fmt.Sprintf("%d", channelAmt)} + create <- creatorCLI.runCmd(paychGetCmd, cmd) + }() + + // Wait for the output to stop being "Channel does not exist" + for regexp.MustCompile(noChannelState).MatchString(out) { + cmd = []string{creatorAddr.String(), receiverAddr.String()} + out = creatorCLI.runCmd(paychStatusCmd, cmd) + } + fmt.Println(out) + + // The next state should be creating channel or channel created, depending + // on timing + stateCreating := regexp.MustCompile("Creating channel").MatchString(out) + stateCreated := regexp.MustCompile("Channel exists").MatchString(out) + require.True(t, stateCreating || stateCreated) + + channelAmtAtto := types.BigMul(types.NewInt(channelAmt), types.NewInt(build.FilecoinPrecision)) + channelAmtStr := fmt.Sprintf("%d", channelAmtAtto) + if stateCreating { + // If we're in the creating state (most likely) the amount should be pending + require.Regexp(t, regexp.MustCompile("Pending.*"+channelAmtStr), out) + } + + // Wait for create channel to complete + chstr := <-create + + cmd = []string{creatorAddr.String(), receiverAddr.String()} + out = creatorCLI.runCmd(paychStatusCmd, cmd) + fmt.Println(out) + // Output should have the channel address + require.Regexp(t, regexp.MustCompile("Channel.*"+chstr), out) + // Output should have confirmed amount + require.Regexp(t, regexp.MustCompile("Confirmed.*"+channelAmtStr), out) + + chAddr, err := address.NewFromString(chstr) + require.NoError(t, err) + + // creator: paych voucher create + voucherAmt := uint64(10) + cmd = []string{chAddr.String(), fmt.Sprintf("%d", voucherAmt)} + creatorCLI.runCmd(paychVoucherCreateCmd, cmd) + + cmd = []string{creatorAddr.String(), receiverAddr.String()} + out = creatorCLI.runCmd(paychStatusCmd, cmd) + fmt.Println(out) + voucherAmtAtto := types.BigMul(types.NewInt(voucherAmt), types.NewInt(build.FilecoinPrecision)) + voucherAmtStr := fmt.Sprintf("%d", voucherAmtAtto) + // Output should include voucher amount + require.Regexp(t, regexp.MustCompile("Voucher.*"+voucherAmtStr), out) +} + // TestPaymentChannelVouchers does a basic test to exercise some payment // channel voucher commands func TestPaymentChannelVouchers(t *testing.T) {