feat: add --csv option to the lotus send cmd (#12892)

This commit is contained in:
Phi-rjan
2025-02-14 07:48:26 +01:00
committed by GitHub
parent 36b1aa3271
commit 97dc1865cf
5 changed files with 140 additions and 155 deletions

View File

@ -2,8 +2,11 @@ package cli
import (
"bytes"
"encoding/csv"
"encoding/hex"
"fmt"
"os"
"strconv"
"strings"
"github.com/urfave/cli/v2"
@ -69,8 +72,16 @@ var SendCmd = &cli.Command{
Name: "force",
Usage: "Deprecated: use global 'force-send'",
},
&cli.StringFlag{
Name: "csv",
Usage: "send multiple transactions from a CSV file (format: Recipient,FIL,Method,Params)",
},
},
Action: func(cctx *cli.Context) error {
if csvFile := cctx.String("csv"); csvFile != "" {
return handleCSVSend(cctx, csvFile)
}
if cctx.IsSet("force") {
fmt.Println("'force' flag is deprecated, use global flag 'force-send'")
}
@ -244,3 +255,130 @@ var SendCmd = &cli.Command{
return nil
},
}
func handleCSVSend(cctx *cli.Context, csvFile string) error {
srv, err := GetFullNodeServices(cctx)
if err != nil {
return err
}
defer srv.Close() //nolint:errcheck
ctx := ReqContext(cctx)
var fromAddr address.Address
if from := cctx.String("from"); from != "" {
addr, err := address.NewFromString(from)
if err != nil {
return err
}
fromAddr = addr
} else {
defaddr, err := srv.FullNodeAPI().WalletDefaultAddress(ctx)
if err != nil {
return fmt.Errorf("failed to get default address: %w", err)
}
fromAddr = defaddr
}
// Print sending address
_, _ = fmt.Fprintf(cctx.App.Writer, "Sending messages from: %s\n", fromAddr.String())
fileReader, err := os.Open(csvFile)
if err != nil {
return xerrors.Errorf("read csv: %w", err)
}
defer func() {
if err := fileReader.Close(); err != nil {
log.Errorf("failed to close csv file: %v", err)
}
}()
r := csv.NewReader(fileReader)
records, err := r.ReadAll()
if err != nil {
return xerrors.Errorf("read csv: %w", err)
}
// Validate header
if len(records) == 0 ||
len(records[0]) != 4 ||
strings.TrimSpace(records[0][0]) != "Recipient" ||
strings.TrimSpace(records[0][1]) != "FIL" ||
strings.TrimSpace(records[0][2]) != "Method" ||
strings.TrimSpace(records[0][3]) != "Params" {
return xerrors.Errorf("expected header row to be \"Recipient,FIL,Method,Params\"")
}
// First pass: validate and build params
var sendParams []SendParams
totalAmount := abi.NewTokenAmount(0)
for i, e := range records[1:] {
if len(e) != 4 {
return xerrors.Errorf("row %d has %d fields, expected 4", i, len(e))
}
var params SendParams
params.From = fromAddr
// Parse recipient
var err error
params.To, err = address.NewFromString(e[0])
if err != nil {
return xerrors.Errorf("failed to parse address in row %d: %w", i, err)
}
// Parse value
val, err := types.ParseFIL(e[1])
if err != nil {
return xerrors.Errorf("failed to parse amount in row %d: %w", i, err)
}
params.Val = abi.TokenAmount(val)
totalAmount = types.BigAdd(totalAmount, params.Val)
// Parse method
method, err := strconv.Atoi(strings.TrimSpace(e[2]))
if err != nil {
return xerrors.Errorf("failed to parse method number in row %d: %w", i, err)
}
params.Method = abi.MethodNum(method)
// Parse params
if strings.TrimSpace(e[3]) != "nil" {
params.Params, err = hex.DecodeString(strings.TrimSpace(e[3]))
if err != nil {
return xerrors.Errorf("failed to parse hex params in row %d: %w", i, err)
}
}
sendParams = append(sendParams, params)
}
// Check sender balance
senderBalance, err := srv.FullNodeAPI().WalletBalance(ctx, fromAddr)
if err != nil {
return xerrors.Errorf("failed to get sender balance: %w", err)
}
if senderBalance.LessThan(totalAmount) {
return xerrors.Errorf("insufficient funds: need %s FIL, have %s FIL",
types.FIL(totalAmount), types.FIL(senderBalance))
}
// Second pass: perform sends
for i, params := range sendParams {
proto, err := srv.MessageForSend(ctx, params)
if err != nil {
return xerrors.Errorf("creating message prototype for row %d: %w", i, err)
}
sm, err := InteractiveSend(ctx, cctx, srv, proto)
if err != nil {
return xerrors.Errorf("sending message for row %d: %w", i, err)
}
fmt.Printf("Sent message %d: %s\n", i, sm.Cid())
}
return nil
}