Skip to content

Commit 97dc186

Browse files
authored
feat: add --csv option to the lotus send cmd (#12892)
1 parent 36b1aa3 commit 97dc186

File tree

5 files changed

+140
-155
lines changed

5 files changed

+140
-155
lines changed

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
- Exposed `StateGetNetworkParams` in the Lotus Gateway API ([filecoin-project/lotus#12881](https://github.com/filecoin-project/lotus/pull/12881))
1313
- **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))
1414
- 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))
15-
1615
- chore: switch to pure-go zstd decoder for snapshot imports. ([filecoin-project/lotus#12857](https://github.com/filecoin-project/lotus/pull/12857))
17-
1816
- feat: automatically detect if the genesis is zstd compressed. ([filecoin-project/lotus#12885](https://github.com/filecoin-project/lotus/pull/12885)
17+
- `lotus send` now supports `--csv` option for sending multiple transactions. ([filecoin-project/lotus#12892](https://github.com/filecoin-project/lotus/pull/12892))
1918

2019
- chore: upgrade to the latest go-f3 and allow F3 chain exchange topics ([filecoin-project/lotus#12893](https://github.com/filecoin-project/lotus/pull/12893)
2120

cli/send.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package cli
22

33
import (
44
"bytes"
5+
"encoding/csv"
56
"encoding/hex"
67
"fmt"
8+
"os"
9+
"strconv"
710
"strings"
811

912
"github.com/urfave/cli/v2"
@@ -69,8 +72,16 @@ var SendCmd = &cli.Command{
6972
Name: "force",
7073
Usage: "Deprecated: use global 'force-send'",
7174
},
75+
&cli.StringFlag{
76+
Name: "csv",
77+
Usage: "send multiple transactions from a CSV file (format: Recipient,FIL,Method,Params)",
78+
},
7279
},
7380
Action: func(cctx *cli.Context) error {
81+
if csvFile := cctx.String("csv"); csvFile != "" {
82+
return handleCSVSend(cctx, csvFile)
83+
}
84+
7485
if cctx.IsSet("force") {
7586
fmt.Println("'force' flag is deprecated, use global flag 'force-send'")
7687
}
@@ -244,3 +255,130 @@ var SendCmd = &cli.Command{
244255
return nil
245256
},
246257
}
258+
259+
func handleCSVSend(cctx *cli.Context, csvFile string) error {
260+
srv, err := GetFullNodeServices(cctx)
261+
if err != nil {
262+
return err
263+
}
264+
defer srv.Close() //nolint:errcheck
265+
266+
ctx := ReqContext(cctx)
267+
268+
var fromAddr address.Address
269+
if from := cctx.String("from"); from != "" {
270+
addr, err := address.NewFromString(from)
271+
if err != nil {
272+
return err
273+
}
274+
fromAddr = addr
275+
} else {
276+
defaddr, err := srv.FullNodeAPI().WalletDefaultAddress(ctx)
277+
if err != nil {
278+
return fmt.Errorf("failed to get default address: %w", err)
279+
}
280+
fromAddr = defaddr
281+
}
282+
283+
// Print sending address
284+
_, _ = fmt.Fprintf(cctx.App.Writer, "Sending messages from: %s\n", fromAddr.String())
285+
286+
fileReader, err := os.Open(csvFile)
287+
if err != nil {
288+
return xerrors.Errorf("read csv: %w", err)
289+
}
290+
defer func() {
291+
if err := fileReader.Close(); err != nil {
292+
log.Errorf("failed to close csv file: %v", err)
293+
}
294+
}()
295+
296+
r := csv.NewReader(fileReader)
297+
records, err := r.ReadAll()
298+
if err != nil {
299+
return xerrors.Errorf("read csv: %w", err)
300+
}
301+
302+
// Validate header
303+
if len(records) == 0 ||
304+
len(records[0]) != 4 ||
305+
strings.TrimSpace(records[0][0]) != "Recipient" ||
306+
strings.TrimSpace(records[0][1]) != "FIL" ||
307+
strings.TrimSpace(records[0][2]) != "Method" ||
308+
strings.TrimSpace(records[0][3]) != "Params" {
309+
return xerrors.Errorf("expected header row to be \"Recipient,FIL,Method,Params\"")
310+
}
311+
312+
// First pass: validate and build params
313+
var sendParams []SendParams
314+
totalAmount := abi.NewTokenAmount(0)
315+
316+
for i, e := range records[1:] {
317+
if len(e) != 4 {
318+
return xerrors.Errorf("row %d has %d fields, expected 4", i, len(e))
319+
}
320+
321+
var params SendParams
322+
params.From = fromAddr
323+
324+
// Parse recipient
325+
var err error
326+
params.To, err = address.NewFromString(e[0])
327+
if err != nil {
328+
return xerrors.Errorf("failed to parse address in row %d: %w", i, err)
329+
}
330+
331+
// Parse value
332+
val, err := types.ParseFIL(e[1])
333+
if err != nil {
334+
return xerrors.Errorf("failed to parse amount in row %d: %w", i, err)
335+
}
336+
params.Val = abi.TokenAmount(val)
337+
totalAmount = types.BigAdd(totalAmount, params.Val)
338+
339+
// Parse method
340+
method, err := strconv.Atoi(strings.TrimSpace(e[2]))
341+
if err != nil {
342+
return xerrors.Errorf("failed to parse method number in row %d: %w", i, err)
343+
}
344+
params.Method = abi.MethodNum(method)
345+
346+
// Parse params
347+
if strings.TrimSpace(e[3]) != "nil" {
348+
params.Params, err = hex.DecodeString(strings.TrimSpace(e[3]))
349+
if err != nil {
350+
return xerrors.Errorf("failed to parse hex params in row %d: %w", i, err)
351+
}
352+
}
353+
354+
sendParams = append(sendParams, params)
355+
}
356+
357+
// Check sender balance
358+
senderBalance, err := srv.FullNodeAPI().WalletBalance(ctx, fromAddr)
359+
if err != nil {
360+
return xerrors.Errorf("failed to get sender balance: %w", err)
361+
}
362+
363+
if senderBalance.LessThan(totalAmount) {
364+
return xerrors.Errorf("insufficient funds: need %s FIL, have %s FIL",
365+
types.FIL(totalAmount), types.FIL(senderBalance))
366+
}
367+
368+
// Second pass: perform sends
369+
for i, params := range sendParams {
370+
proto, err := srv.MessageForSend(ctx, params)
371+
if err != nil {
372+
return xerrors.Errorf("creating message prototype for row %d: %w", i, err)
373+
}
374+
375+
sm, err := InteractiveSend(ctx, cctx, srv, proto)
376+
if err != nil {
377+
return xerrors.Errorf("sending message for row %d: %w", i, err)
378+
}
379+
380+
fmt.Printf("Sent message %d: %s\n", i, sm.Cid())
381+
}
382+
383+
return nil
384+
}

cmd/lotus-shed/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ func main() {
8282
fr32Cmd,
8383
chainCmd,
8484
balancerCmd,
85-
sendCsvCmd,
8685
terminationsCmd,
8786
migrationsCmd,
8887
diffCmd,

cmd/lotus-shed/send-csv.go

Lines changed: 0 additions & 152 deletions
This file was deleted.

documentation/en/cli-lotus.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ OPTIONS:
198198
--params-json value specify invocation parameters in json
199199
--params-hex value specify invocation parameters in hex
200200
--force Deprecated: use global 'force-send' (default: false)
201+
--csv value send multiple transactions from a CSV file (format: Recipient,FIL,Method,Params)
201202
--help, -h show help
202203
```
203204

0 commit comments

Comments
 (0)