From 2a01d8e730995aeb97ee6b8ba9d1d1834faa3359 Mon Sep 17 00:00:00 2001 From: Phi Date: Tue, 11 Feb 2025 13:39:55 +0100 Subject: [PATCH 1/7] feat: add --csv flag to lotus send feat: add --csv flag to lotus send --- cli/send.go | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/cli/send.go b/cli/send.go index bdf9549bd4c..5036a1fc549 100644 --- a/cli/send.go +++ b/cli/send.go @@ -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,107 @@ 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) + + // Add logic to handle --from address + 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 || + 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\"") + } + + for i, e := range records[1:] { + var params SendParams + + // Set the from address + params.From = fromAddr + + // Parse recipient + 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) + + // 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) + } + } + + // Use existing send logic + 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 +} From 09b85ec3d5a01e18ad4bb4fe55cfec38707995f4 Mon Sep 17 00:00:00 2001 From: Phi Date: Tue, 11 Feb 2025 17:32:59 +0100 Subject: [PATCH 2/7] chore: make docsgen-cli chore: make gen and make docsgen-cli --- documentation/en/cli-lotus.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/en/cli-lotus.md b/documentation/en/cli-lotus.md index 0d760766b2b..6cf9833089d 100644 --- a/documentation/en/cli-lotus.md +++ b/documentation/en/cli-lotus.md @@ -198,6 +198,7 @@ OPTIONS: --params-json value specify invocation parameters in json --params-hex value specify invocation parameters in hex --force Deprecated: use global 'force-send' (default: false) + --csv value send multiple transactions from a CSV file (format: Recipient,FIL,Method,Params) --help, -h show help ``` From d5e071d4bc0ec1bb647d4d4e16c3247c54e5d68e Mon Sep 17 00:00:00 2001 From: Phi Date: Wed, 12 Feb 2025 08:40:52 +0100 Subject: [PATCH 3/7] chore: update changelog chore: update changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08bdd3fa86f..a9e5c770e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,9 @@ - Exposed `StateGetNetworkParams` in the Lotus Gateway API ([filecoin-project/lotus#12881](https://github.com/filecoin-project/lotus/pull/12881)) - **BREAKING**: Removed `SupportedProofTypes` from `StateGetNetworkParams` response as it was unreliable and didn't match FVM's actual supported proofs ([filecoin-project/lotus#12881](https://github.com/filecoin-project/lotus/pull/12881)) - refactor(eth): attach ToFilecoinMessage converter to EthCall method for improved package/module import structure. This change also exports the converter as a public method, enhancing usability for developers utilizing Lotus as a library. ([filecoin-project/lotus#12844](https://github.com/filecoin-project/lotus/pull/12844)) - - chore: switch to pure-go zstd decoder for snapshot imports. ([filecoin-project/lotus#12857](https://github.com/filecoin-project/lotus/pull/12857)) - - feat: automatically detect if the genesis is zstd compressed. ([filecoin-project/lotus#12885](https://github.com/filecoin-project/lotus/pull/12885) +- `lotus send` now supports `--csv` option for sending multiple transactions. ([filecoin-project/lotus#12892](https://github.com/filecoin-project/lotus/pull/12892)) # UNRELEASED v.1.32.0 From 3be8496b8f89458594e8adce7206838610f1e28f Mon Sep 17 00:00:00 2001 From: Phi-rjan Date: Thu, 13 Feb 2025 08:29:08 +0100 Subject: [PATCH 4/7] Update cli/send.go Co-authored-by: Rod Vagg --- cli/send.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/send.go b/cli/send.go index 5036a1fc549..577c7f951ec 100644 --- a/cli/send.go +++ b/cli/send.go @@ -302,6 +302,7 @@ func handleCSVSend(cctx *cli.Context, csvFile string) error { // 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" || From fcad102567cd33da8f188c2434ee70a255ae61b2 Mon Sep 17 00:00:00 2001 From: Phi Date: Thu, 13 Feb 2025 08:52:30 +0100 Subject: [PATCH 5/7] feat(cli): add two-pass CSV send with balance check feat(cli): add two-pass CSV send with balance check --- cli/send.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/cli/send.go b/cli/send.go index 577c7f951ec..263b187e028 100644 --- a/cli/send.go +++ b/cli/send.go @@ -310,13 +310,20 @@ func handleCSVSend(cctx *cli.Context, csvFile string) error { 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:] { - var params SendParams + if len(e) != 4 { + return xerrors.Errorf("row %d has %d fields, expected 4", i, len(e)) + } - // Set the from address + 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) @@ -328,6 +335,7 @@ func handleCSVSend(cctx *cli.Context, csvFile string) error { 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])) @@ -344,7 +352,22 @@ func handleCSVSend(cctx *cli.Context, csvFile string) error { } } - // Use existing send logic + 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) From 850127cbf7c7b5076ffb92aab242aec745baded5 Mon Sep 17 00:00:00 2001 From: Phi Date: Thu, 13 Feb 2025 08:54:44 +0100 Subject: [PATCH 6/7] refactor: remove send-csv from lotus-shed refactor: remove send-csv from lotus-shed --- cmd/lotus-shed/main.go | 1 - cmd/lotus-shed/send-csv.go | 152 ------------------------------------- 2 files changed, 153 deletions(-) delete mode 100644 cmd/lotus-shed/send-csv.go diff --git a/cmd/lotus-shed/main.go b/cmd/lotus-shed/main.go index 615c94338b3..00c477c20c1 100644 --- a/cmd/lotus-shed/main.go +++ b/cmd/lotus-shed/main.go @@ -82,7 +82,6 @@ func main() { fr32Cmd, chainCmd, balancerCmd, - sendCsvCmd, terminationsCmd, migrationsCmd, diffCmd, diff --git a/cmd/lotus-shed/send-csv.go b/cmd/lotus-shed/send-csv.go deleted file mode 100644 index 17b62150fc5..00000000000 --- a/cmd/lotus-shed/send-csv.go +++ /dev/null @@ -1,152 +0,0 @@ -package main - -import ( - "encoding/csv" - "encoding/hex" - "fmt" - "os" - "strconv" - "strings" - - "github.com/ipfs/go-cid" - "github.com/urfave/cli/v2" - "golang.org/x/xerrors" - - "github.com/filecoin-project/go-address" - "github.com/filecoin-project/go-state-types/abi" - "github.com/filecoin-project/go-state-types/exitcode" - - lapi "github.com/filecoin-project/lotus/api" - "github.com/filecoin-project/lotus/chain/types" - lcli "github.com/filecoin-project/lotus/cli" -) - -var sendCsvCmd = &cli.Command{ - Name: "send-csv", - Usage: "Utility for sending a batch of balance transfers", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "from", - Usage: "specify the account to send funds from", - Required: true, - }, - }, - ArgsUsage: "[csvfile]", - Action: func(cctx *cli.Context) error { - if cctx.NArg() != 1 { - return lcli.IncorrectNumArgs(cctx) - } - - api, closer, err := lcli.GetFullNodeAPIV1(cctx) - if err != nil { - return err - } - - defer closer() - ctx := lcli.ReqContext(cctx) - - srv, err := lcli.GetFullNodeServices(cctx) - if err != nil { - return err - } - defer srv.Close() //nolint:errcheck - - sender, err := address.NewFromString(cctx.String("from")) - if err != nil { - return err - } - - fileReader, err := os.Open(cctx.Args().First()) - if err != nil { - return xerrors.Errorf("read csv: %w", err) - } - - defer fileReader.Close() //nolint:errcheck - r := csv.NewReader(fileReader) - records, err := r.ReadAll() - if err != nil { - return xerrors.Errorf("read csv: %w", err) - } - - if 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\"") - } - - var msgs []*types.Message - for i, e := range records[1:] { - addr, err := address.NewFromString(e[0]) - if err != nil { - return xerrors.Errorf("failed to parse address in row %d: %w", i, err) - } - - value, err := types.ParseFIL(strings.TrimSpace(e[1])) - if err != nil { - return xerrors.Errorf("failed to parse value balance: %w", err) - } - - method, err := strconv.Atoi(strings.TrimSpace(e[2])) - if err != nil { - return xerrors.Errorf("failed to parse method number: %w", err) - } - - var params []byte - if strings.TrimSpace(e[3]) != "nil" { - params, err = hex.DecodeString(strings.TrimSpace(e[3])) - if err != nil { - return xerrors.Errorf("failed to parse hexparams: %w", err) - } - } - - msgs = append(msgs, &types.Message{ - To: addr, - From: sender, - Value: abi.TokenAmount(value), - Method: abi.MethodNum(method), - Params: params, - }) - } - - if len(msgs) == 0 { - return nil - } - - var msgCids []cid.Cid - for i, msg := range msgs { - smsg, err := api.MpoolPushMessage(ctx, msg, nil) - if err != nil { - fmt.Printf("%d, ERROR %s\n", i, err) - continue - } - - fmt.Printf("%d, %s\n", i, smsg.Cid()) - - if i > 0 && i%100 == 0 { - fmt.Printf("catching up until latest message lands") - _, err := api.StateWaitMsg(ctx, smsg.Cid(), 1, lapi.LookbackNoLimit, true) - if err != nil { - return err - } - } - - msgCids = append(msgCids, smsg.Cid()) - } - - fmt.Println("waiting on messages...") - - for _, msgCid := range msgCids { - ml, err := api.StateWaitMsg(ctx, msgCid, 5, lapi.LookbackNoLimit, true) - if err != nil { - return err - } - if ml.Receipt.ExitCode != exitcode.Ok { - fmt.Printf("MSG %s NON-ZERO EXITCODE: %s\n", msgCid, ml.Receipt.ExitCode) - } - } - - fmt.Println("all sent messages succeeded") - return nil - }, -} From 0e99c651169126dc3ffbd42a1087cdf52600d946 Mon Sep 17 00:00:00 2001 From: Phi-rjan Date: Fri, 14 Feb 2025 07:05:59 +0100 Subject: [PATCH 7/7] Update send.go Co-authored-by: Rod Vagg --- cli/send.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/send.go b/cli/send.go index 263b187e028..86288778e4c 100644 --- a/cli/send.go +++ b/cli/send.go @@ -265,7 +265,6 @@ func handleCSVSend(cctx *cli.Context, csvFile string) error { ctx := ReqContext(cctx) - // Add logic to handle --from address var fromAddr address.Address if from := cctx.String("from"); from != "" { addr, err := address.NewFromString(from)