2020-01-21 19:02:51 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2020-02-04 03:37:55 +00:00
|
|
|
"context"
|
|
|
|
"encoding/binary"
|
2020-01-21 19:02:51 +00:00
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
2020-02-04 03:37:55 +00:00
|
|
|
"sort"
|
2020-01-21 19:02:51 +00:00
|
|
|
"strconv"
|
|
|
|
"text/tabwriter"
|
|
|
|
|
|
|
|
"github.com/filecoin-project/go-address"
|
2020-02-13 03:50:37 +00:00
|
|
|
init_ "github.com/filecoin-project/specs-actors/actors/builtin/init"
|
2020-02-06 19:49:21 +00:00
|
|
|
samsig "github.com/filecoin-project/specs-actors/actors/builtin/multisig"
|
2020-02-04 03:37:55 +00:00
|
|
|
cid "github.com/ipfs/go-cid"
|
|
|
|
"github.com/ipfs/go-hamt-ipld"
|
2020-02-04 22:19:05 +00:00
|
|
|
cbor "github.com/ipfs/go-ipld-cbor"
|
2020-06-05 22:59:01 +00:00
|
|
|
"github.com/urfave/cli/v2"
|
2020-01-21 19:02:51 +00:00
|
|
|
cbg "github.com/whyrusleeping/cbor-gen"
|
2020-02-04 03:37:55 +00:00
|
|
|
"golang.org/x/xerrors"
|
2020-02-06 19:49:21 +00:00
|
|
|
|
|
|
|
"github.com/filecoin-project/lotus/api"
|
|
|
|
"github.com/filecoin-project/lotus/api/apibstore"
|
2020-06-04 02:34:57 +00:00
|
|
|
"github.com/filecoin-project/lotus/build"
|
2020-02-06 19:49:21 +00:00
|
|
|
types "github.com/filecoin-project/lotus/chain/types"
|
2020-01-21 19:02:51 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var multisigCmd = &cli.Command{
|
|
|
|
Name: "msig",
|
|
|
|
Usage: "Interact with a multisig wallet",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "source",
|
|
|
|
Usage: "specify the account to send propose from",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Subcommands: []*cli.Command{
|
|
|
|
msigCreateCmd,
|
|
|
|
msigInspectCmd,
|
|
|
|
msigProposeCmd,
|
|
|
|
msigApproveCmd,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var msigCreateCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "create",
|
|
|
|
Usage: "Create a new multisig wallet",
|
2020-03-04 21:46:00 +00:00
|
|
|
ArgsUsage: "[address1 address2 ...]",
|
2020-01-21 19:02:51 +00:00
|
|
|
Flags: []cli.Flag{
|
2020-02-04 03:37:55 +00:00
|
|
|
&cli.Int64Flag{
|
2020-01-21 19:02:51 +00:00
|
|
|
Name: "required",
|
|
|
|
},
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "value",
|
|
|
|
Usage: "initial funds to give to multisig",
|
|
|
|
Value: "0",
|
|
|
|
},
|
2020-04-24 21:55:33 +00:00
|
|
|
&cli.StringFlag{
|
2020-07-07 21:37:43 +00:00
|
|
|
Name: "from",
|
2020-04-24 21:55:33 +00:00
|
|
|
Usage: "account to send the create message from",
|
|
|
|
},
|
2020-01-21 19:02:51 +00:00
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
2020-07-06 16:40:56 +00:00
|
|
|
if cctx.Args().Len() < 1 {
|
|
|
|
return fmt.Errorf("multisigs must have at least one signer")
|
|
|
|
}
|
|
|
|
|
2020-01-21 19:02:51 +00:00
|
|
|
var addrs []address.Address
|
|
|
|
for _, a := range cctx.Args().Slice() {
|
|
|
|
addr, err := address.NewFromString(a)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
addrs = append(addrs, addr)
|
|
|
|
}
|
|
|
|
|
|
|
|
// get the address we're going to use to create the multisig (can be one of the above, as long as they have funds)
|
2020-04-24 21:55:33 +00:00
|
|
|
var sendAddr address.Address
|
2020-07-07 21:37:43 +00:00
|
|
|
if send := cctx.String("from"); send == "" {
|
2020-04-24 21:55:33 +00:00
|
|
|
defaddr, err := api.WalletDefaultAddress(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sendAddr = defaddr
|
|
|
|
} else {
|
|
|
|
addr, err := address.NewFromString(send)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sendAddr = addr
|
2020-01-21 19:02:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
val := cctx.String("value")
|
|
|
|
filval, err := types.ParseFIL(val)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-05-04 19:29:26 +00:00
|
|
|
intVal := types.BigInt(filval)
|
|
|
|
|
2020-06-26 13:52:22 +00:00
|
|
|
required := cctx.Uint64("required")
|
2020-01-21 19:02:51 +00:00
|
|
|
if required == 0 {
|
2020-06-26 13:52:22 +00:00
|
|
|
required = uint64(len(addrs))
|
2020-01-21 19:02:51 +00:00
|
|
|
}
|
|
|
|
|
2020-05-04 19:29:26 +00:00
|
|
|
gp := types.NewInt(1)
|
2020-01-21 19:02:51 +00:00
|
|
|
|
2020-05-04 19:29:26 +00:00
|
|
|
msgCid, err := api.MsigCreate(ctx, required, addrs, intVal, sendAddr, gp)
|
2020-01-21 19:02:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// wait for it to get mined into a block
|
2020-06-03 21:42:06 +00:00
|
|
|
wait, err := api.StateWaitMsg(ctx, msgCid, build.MessageConfidence)
|
2020-01-21 19:02:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// check it executed successfully
|
|
|
|
if wait.Receipt.ExitCode != 0 {
|
|
|
|
fmt.Println("actor creation failed!")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// get address of newly created miner
|
2020-04-14 09:18:19 +00:00
|
|
|
|
|
|
|
var execreturn init_.ExecReturn
|
|
|
|
if err := execreturn.UnmarshalCBOR(bytes.NewReader(wait.Receipt.Return)); err != nil {
|
2020-01-21 19:02:51 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-04-14 09:18:19 +00:00
|
|
|
fmt.Println("Created new multisig: ", execreturn.IDAddress, execreturn.RobustAddress)
|
2020-04-17 16:27:11 +00:00
|
|
|
|
2020-01-21 19:52:59 +00:00
|
|
|
// TODO: maybe register this somewhere
|
2020-01-21 19:02:51 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var msigInspectCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "inspect",
|
|
|
|
Usage: "Inspect a multisig wallet",
|
2020-03-04 21:46:00 +00:00
|
|
|
ArgsUsage: "[address]",
|
2020-03-06 19:01:28 +00:00
|
|
|
Flags: []cli.Flag{},
|
2020-01-21 19:02:51 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
if !cctx.Args().Present() {
|
|
|
|
return fmt.Errorf("must specify address of multisig to inspect")
|
|
|
|
}
|
|
|
|
|
|
|
|
maddr, err := address.NewFromString(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-02-11 23:29:45 +00:00
|
|
|
act, err := api.StateGetActor(ctx, maddr, types.EmptyTSK)
|
2020-01-21 19:02:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
obj, err := api.ChainReadObj(ctx, act.Head)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-02-08 02:18:32 +00:00
|
|
|
var mstate samsig.State
|
2020-01-21 19:02:51 +00:00
|
|
|
if err := mstate.UnmarshalCBOR(bytes.NewReader(obj)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Balance: %sfil\n", types.FIL(act.Balance))
|
2020-02-04 03:37:55 +00:00
|
|
|
fmt.Printf("Threshold: %d / %d\n", mstate.NumApprovalsThreshold, len(mstate.Signers))
|
2020-01-21 19:02:51 +00:00
|
|
|
fmt.Println("Signers:")
|
|
|
|
for _, s := range mstate.Signers {
|
|
|
|
fmt.Printf("\t%s\n", s)
|
|
|
|
}
|
2020-02-04 03:37:55 +00:00
|
|
|
|
|
|
|
pending, err := GetMultisigPending(ctx, api, mstate.PendingTxns)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("reading pending transactions: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("Transactions: ", len(pending))
|
|
|
|
if len(pending) > 0 {
|
|
|
|
var txids []int64
|
|
|
|
for txid := range pending {
|
|
|
|
txids = append(txids, txid)
|
|
|
|
}
|
|
|
|
sort.Slice(txids, func(i, j int) bool {
|
|
|
|
return txids[i] < txids[j]
|
|
|
|
})
|
|
|
|
|
2020-01-21 19:02:51 +00:00
|
|
|
w := tabwriter.NewWriter(os.Stdout, 8, 4, 0, ' ', 0)
|
|
|
|
fmt.Fprintf(w, "ID\tState\tTo\tValue\tMethod\tParams\n")
|
2020-02-04 03:37:55 +00:00
|
|
|
for _, txid := range txids {
|
|
|
|
tx := pending[txid]
|
|
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%d\t%x\n", txid, state(tx), tx.To, types.FIL(tx.Value), tx.Method, tx.Params)
|
2020-01-21 19:02:51 +00:00
|
|
|
}
|
2020-05-27 20:53:20 +00:00
|
|
|
if err := w.Flush(); err != nil {
|
2020-05-27 23:15:19 +00:00
|
|
|
return xerrors.Errorf("flushing output: %+v", err)
|
2020-05-27 20:53:20 +00:00
|
|
|
}
|
|
|
|
|
2020-01-21 19:02:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-02-08 02:18:32 +00:00
|
|
|
func GetMultisigPending(ctx context.Context, lapi api.FullNode, hroot cid.Cid) (map[int64]*samsig.Transaction, error) {
|
2020-02-04 03:37:55 +00:00
|
|
|
bs := apibstore.NewAPIBlockstore(lapi)
|
2020-02-04 22:19:05 +00:00
|
|
|
cst := cbor.NewCborStore(bs)
|
2020-02-04 03:37:55 +00:00
|
|
|
|
2020-02-14 14:14:39 +00:00
|
|
|
nd, err := hamt.LoadNode(ctx, cst, hroot, hamt.UseTreeBitWidth(5))
|
2020-02-04 03:37:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-02-08 02:18:32 +00:00
|
|
|
txs := make(map[int64]*samsig.Transaction)
|
2020-02-04 03:37:55 +00:00
|
|
|
err = nd.ForEach(ctx, func(k string, val interface{}) error {
|
|
|
|
d := val.(*cbg.Deferred)
|
2020-02-08 02:18:32 +00:00
|
|
|
var tx samsig.Transaction
|
2020-02-04 03:37:55 +00:00
|
|
|
if err := tx.UnmarshalCBOR(bytes.NewReader(d.Raw)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
txid, _ := binary.Varint([]byte(k))
|
|
|
|
|
|
|
|
txs[txid] = &tx
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, xerrors.Errorf("failed to iterate transactions hamt: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return txs, nil
|
|
|
|
}
|
|
|
|
|
2020-02-08 02:18:32 +00:00
|
|
|
func state(tx *samsig.Transaction) string {
|
2020-02-04 03:37:55 +00:00
|
|
|
/* // TODO(why): I strongly disagree with not having these... but i need to move forward
|
2020-01-21 19:02:51 +00:00
|
|
|
if tx.Complete {
|
|
|
|
return "done"
|
|
|
|
}
|
|
|
|
if tx.Canceled {
|
|
|
|
return "canceled"
|
|
|
|
}
|
2020-02-04 03:37:55 +00:00
|
|
|
*/
|
2020-01-21 19:02:51 +00:00
|
|
|
return "pending"
|
|
|
|
}
|
|
|
|
|
|
|
|
var msigProposeCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "propose",
|
|
|
|
Usage: "Propose a multisig transaction",
|
2020-04-25 05:39:25 +00:00
|
|
|
ArgsUsage: "[multisigAddress destinationAddress value <methodId methodParams> (optional)]",
|
2020-04-24 22:00:31 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "source",
|
|
|
|
Usage: "account to send the propose message from",
|
|
|
|
},
|
|
|
|
},
|
2020-01-21 19:02:51 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
if cctx.Args().Len() < 3 {
|
|
|
|
return fmt.Errorf("must pass multisig address, destination, and value")
|
|
|
|
}
|
|
|
|
|
|
|
|
if cctx.Args().Len() > 3 && cctx.Args().Len() != 5 {
|
|
|
|
return fmt.Errorf("usage: msig propose <msig addr> <desination> <value> [ <method> <params> ]")
|
|
|
|
}
|
|
|
|
|
|
|
|
msig, err := address.NewFromString(cctx.Args().Get(0))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
dest, err := address.NewFromString(cctx.Args().Get(1))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
value, err := types.ParseFIL(cctx.Args().Get(2))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var method uint64
|
|
|
|
var params []byte
|
|
|
|
if cctx.Args().Len() == 5 {
|
|
|
|
m, err := strconv.ParseUint(cctx.Args().Get(3), 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
method = m
|
|
|
|
|
|
|
|
p, err := hex.DecodeString(cctx.Args().Get(4))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
params = p
|
|
|
|
}
|
|
|
|
|
|
|
|
var from address.Address
|
|
|
|
if cctx.IsSet("source") {
|
|
|
|
f, err := address.NewFromString(cctx.String("source"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
from = f
|
|
|
|
} else {
|
|
|
|
defaddr, err := api.WalletDefaultAddress(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
from = defaddr
|
|
|
|
}
|
|
|
|
|
2020-05-04 19:29:26 +00:00
|
|
|
msgCid, err := api.MsigPropose(ctx, msig, dest, types.BigInt(value), from, method, params)
|
2020-01-21 19:02:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-05-04 19:29:26 +00:00
|
|
|
fmt.Println("send proposal in message: ", msgCid)
|
2020-01-21 19:02:51 +00:00
|
|
|
|
2020-06-03 21:42:06 +00:00
|
|
|
wait, err := api.StateWaitMsg(ctx, msgCid, build.MessageConfidence)
|
2020-01-21 19:02:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if wait.Receipt.ExitCode != 0 {
|
|
|
|
return fmt.Errorf("proposal returned exit %d", wait.Receipt.ExitCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, v, err := cbg.CborReadHeader(bytes.NewReader(wait.Receipt.Return))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Transaction ID: %d\n", v)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var msigApproveCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "approve",
|
|
|
|
Usage: "Approve a multisig message",
|
2020-04-25 05:39:25 +00:00
|
|
|
ArgsUsage: "[multisigAddress messageId proposerAddress destination value <methodId methodParams> (optional)]",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "source",
|
|
|
|
Usage: "account to send the approve message from",
|
|
|
|
},
|
|
|
|
},
|
2020-01-21 19:02:51 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
2020-04-25 05:39:25 +00:00
|
|
|
if cctx.Args().Len() < 5 {
|
|
|
|
return fmt.Errorf("must pass multisig address, message ID, proposer address, destination, and value")
|
|
|
|
}
|
|
|
|
|
|
|
|
if cctx.Args().Len() > 5 && cctx.Args().Len() != 7 {
|
|
|
|
return fmt.Errorf("usage: msig approve <msig addr> <message ID> <proposer address> <desination> <value> [ <method> <params> ]")
|
2020-01-21 19:02:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
msig, err := address.NewFromString(cctx.Args().Get(0))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
txid, err := strconv.ParseUint(cctx.Args().Get(1), 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-04-25 05:39:25 +00:00
|
|
|
proposer, err := address.NewFromString(cctx.Args().Get(2))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if proposer.Protocol() != address.ID {
|
|
|
|
proposer, err = api.StateLookupID(ctx, proposer, types.EmptyTSK)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dest, err := address.NewFromString(cctx.Args().Get(3))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
value, err := types.ParseFIL(cctx.Args().Get(4))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var method uint64
|
|
|
|
var params []byte
|
|
|
|
if cctx.Args().Len() == 7 {
|
|
|
|
m, err := strconv.ParseUint(cctx.Args().Get(5), 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
method = m
|
|
|
|
|
|
|
|
p, err := hex.DecodeString(cctx.Args().Get(6))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
params = p
|
|
|
|
}
|
|
|
|
|
2020-01-21 19:02:51 +00:00
|
|
|
var from address.Address
|
|
|
|
if cctx.IsSet("source") {
|
|
|
|
f, err := address.NewFromString(cctx.String("source"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
from = f
|
|
|
|
} else {
|
|
|
|
defaddr, err := api.WalletDefaultAddress(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
from = defaddr
|
|
|
|
}
|
|
|
|
|
2020-05-04 19:29:26 +00:00
|
|
|
msgCid, err := api.MsigApprove(ctx, msig, txid, proposer, dest, types.BigInt(value), from, method, params)
|
2020-01-21 19:02:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-05-04 19:29:26 +00:00
|
|
|
fmt.Println("sent approval in message: ", msgCid)
|
2020-01-21 19:02:51 +00:00
|
|
|
|
2020-06-03 21:42:06 +00:00
|
|
|
wait, err := api.StateWaitMsg(ctx, msgCid, build.MessageConfidence)
|
2020-01-21 19:02:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if wait.Receipt.ExitCode != 0 {
|
|
|
|
return fmt.Errorf("approve returned exit %d", wait.Receipt.ExitCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|