2023-01-13 19:11:13 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
|
|
|
|
"github.com/urfave/cli/v2"
|
|
|
|
cbg "github.com/whyrusleeping/cbor-gen"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
|
|
|
|
"github.com/filecoin-project/go-address"
|
|
|
|
amt4 "github.com/filecoin-project/go-amt-ipld/v4"
|
|
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
|
|
"github.com/filecoin-project/go-state-types/big"
|
|
|
|
builtintypes "github.com/filecoin-project/go-state-types/builtin"
|
|
|
|
"github.com/filecoin-project/go-state-types/builtin/v10/eam"
|
|
|
|
|
|
|
|
"github.com/filecoin-project/lotus/api/v0api"
|
|
|
|
"github.com/filecoin-project/lotus/chain/actors"
|
2023-02-17 05:21:29 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/actors/builtin"
|
2023-01-13 19:11:13 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
|
|
"github.com/filecoin-project/lotus/chain/types/ethtypes"
|
|
|
|
)
|
|
|
|
|
|
|
|
var EvmCmd = &cli.Command{
|
|
|
|
Name: "evm",
|
|
|
|
Usage: "Commands related to the Filecoin EVM runtime",
|
|
|
|
Subcommands: []*cli.Command{
|
|
|
|
EvmDeployCmd,
|
|
|
|
EvmInvokeCmd,
|
|
|
|
EvmGetInfoCmd,
|
|
|
|
EvmCallSimulateCmd,
|
|
|
|
EvmGetContractAddress,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var EvmGetInfoCmd = &cli.Command{
|
2023-01-20 21:23:56 +00:00
|
|
|
Name: "stat",
|
|
|
|
Usage: "Print eth/filecoin addrs and code cid",
|
|
|
|
ArgsUsage: "address",
|
2023-01-13 19:11:13 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
2023-01-20 21:23:56 +00:00
|
|
|
if cctx.NArg() != 1 {
|
|
|
|
return IncorrectNumArgs(cctx)
|
|
|
|
}
|
2023-01-13 19:11:13 +00:00
|
|
|
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
2023-01-20 21:23:56 +00:00
|
|
|
addrString := cctx.Args().Get(0)
|
|
|
|
|
|
|
|
var faddr address.Address
|
|
|
|
var eaddr ethtypes.EthAddress
|
|
|
|
addr, err := address.NewFromString(addrString)
|
|
|
|
if err != nil { // This isn't a filecoin address
|
|
|
|
eaddr, err = ethtypes.ParseEthAddress(addrString)
|
|
|
|
if err != nil { // This isn't an Eth address either
|
|
|
|
return xerrors.Errorf("address is not a filecoin or eth address")
|
2023-01-13 19:11:13 +00:00
|
|
|
}
|
2023-01-20 21:23:56 +00:00
|
|
|
faddr, err = eaddr.ToFilecoinAddress()
|
2023-01-13 19:11:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-01-20 21:23:56 +00:00
|
|
|
} else {
|
|
|
|
eaddr, faddr, err = ethAddrFromFilecoinAddress(ctx, addr, api)
|
2023-01-13 19:11:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
actor, err := api.StateGetActor(ctx, faddr, types.EmptyTSK)
|
|
|
|
fmt.Println("Filecoin address: ", faddr)
|
|
|
|
fmt.Println("Eth address: ", eaddr)
|
2023-02-17 04:58:16 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Actor lookup failed for faddr %s with error: %s\n", faddr, err)
|
|
|
|
} else {
|
|
|
|
idAddr, err := api.StateLookupID(ctx, faddr, types.EmptyTSK)
|
|
|
|
if err == nil {
|
|
|
|
fmt.Println("ID address: ", idAddr)
|
|
|
|
fmt.Println("Code cid: ", actor.Code.String())
|
|
|
|
fmt.Println("Actor Type: ", builtin.ActorNameByCode(actor.Code))
|
|
|
|
}
|
|
|
|
}
|
2023-01-13 19:11:13 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var EvmCallSimulateCmd = &cli.Command{
|
|
|
|
Name: "call",
|
|
|
|
Usage: "Simulate an eth contract call",
|
|
|
|
ArgsUsage: "[from] [to] [params]",
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
|
|
|
|
if cctx.NArg() != 3 {
|
|
|
|
return IncorrectNumArgs(cctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
fromEthAddr, err := ethtypes.ParseEthAddress(cctx.Args().Get(0))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
toEthAddr, err := ethtypes.ParseEthAddress(cctx.Args().Get(1))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-02-09 17:49:08 +00:00
|
|
|
params, err := ethtypes.DecodeHexStringTrimSpace(cctx.Args().Get(2))
|
2023-01-13 19:11:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
api, closer, err := GetFullNodeAPIV1(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
res, err := api.EthCall(ctx, ethtypes.EthCall{
|
|
|
|
From: &fromEthAddr,
|
|
|
|
To: &toEthAddr,
|
|
|
|
Data: params,
|
|
|
|
}, "")
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("Eth call fails, return val: ", res)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("Result: ", res)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var EvmGetContractAddress = &cli.Command{
|
|
|
|
Name: "contract-address",
|
|
|
|
Usage: "Generate contract address from smart contract code",
|
|
|
|
ArgsUsage: "[senderEthAddr] [salt] [contractHexPath]",
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
|
|
|
|
if cctx.NArg() != 3 {
|
|
|
|
return IncorrectNumArgs(cctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
sender, err := ethtypes.ParseEthAddress(cctx.Args().Get(0))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-02-09 17:49:08 +00:00
|
|
|
salt, err := ethtypes.DecodeHexStringTrimSpace(cctx.Args().Get(1))
|
2023-01-13 19:11:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("Could not decode salt: %w", err)
|
|
|
|
}
|
|
|
|
if len(salt) > 32 {
|
|
|
|
return xerrors.Errorf("Len of salt bytes greater than 32")
|
|
|
|
}
|
|
|
|
var fsalt [32]byte
|
|
|
|
copy(fsalt[:], salt[:])
|
|
|
|
|
|
|
|
contractBin := cctx.Args().Get(2)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
contractHex, err := os.ReadFile(contractBin)
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
2023-02-09 17:49:08 +00:00
|
|
|
contract, err := ethtypes.DecodeHexStringTrimSpace(string(contractHex))
|
2023-01-13 19:11:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("Could not decode contract file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
contractAddr, err := ethtypes.GetContractEthAddressFromCode(sender, fsalt, contract)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("Contract Eth address: ", contractAddr)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var EvmDeployCmd = &cli.Command{
|
|
|
|
Name: "deploy",
|
|
|
|
Usage: "Deploy an EVM smart contract and return its address",
|
|
|
|
ArgsUsage: "contract",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "from",
|
|
|
|
Usage: "optionally specify the account to use for sending the creation message",
|
|
|
|
},
|
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "hex",
|
|
|
|
Usage: "use when input contract is in hex",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
afmt := NewAppFmt(cctx.App)
|
|
|
|
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
if argc := cctx.Args().Len(); argc != 1 {
|
|
|
|
return xerrors.Errorf("must pass the contract init code")
|
|
|
|
}
|
|
|
|
|
|
|
|
contract, err := os.ReadFile(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to read contract: %w", err)
|
|
|
|
}
|
|
|
|
if cctx.Bool("hex") {
|
2023-02-09 17:49:08 +00:00
|
|
|
contract, err = ethtypes.DecodeHexStringTrimSpace(string(contract))
|
2023-01-13 19:11:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to decode contract: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var fromAddr address.Address
|
|
|
|
if from := cctx.String("from"); from == "" {
|
|
|
|
fromAddr, err = api.WalletDefaultAddress(ctx)
|
|
|
|
} else {
|
|
|
|
fromAddr, err = address.NewFromString(from)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-13 20:31:08 +00:00
|
|
|
initcode := abi.CborBytes(contract)
|
|
|
|
params, err := actors.SerializeParams(&initcode)
|
2023-01-13 19:11:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to serialize Create params: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
msg := &types.Message{
|
|
|
|
To: builtintypes.EthereumAddressManagerActorAddr,
|
|
|
|
From: fromAddr,
|
|
|
|
Value: big.Zero(),
|
2023-01-13 20:31:08 +00:00
|
|
|
Method: builtintypes.MethodsEAM.CreateExternal,
|
2023-01-13 19:11:13 +00:00
|
|
|
Params: params,
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: On Jan 11th, we decided to add an `EAM#create_external` method
|
|
|
|
// that uses the nonce of the caller instead of taking a user-supplied nonce.
|
|
|
|
// Track: https://github.com/filecoin-project/ref-fvm/issues/1255
|
|
|
|
// When that's implemented, we should migrate the CLI to use that,
|
|
|
|
// as `EAM#create` will be reserved for the EVM runtime actor.
|
|
|
|
// TODO: this is very racy. It may assign a _different_ nonce than the expected one.
|
|
|
|
afmt.Println("sending message...")
|
|
|
|
smsg, err := api.MpoolPushMessage(ctx, msg, nil)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to push message: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
afmt.Println("waiting for message to execute...")
|
|
|
|
wait, err := api.StateWaitMsg(ctx, smsg.Cid(), 0)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("error waiting for message: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check it executed successfully
|
|
|
|
if wait.Receipt.ExitCode != 0 {
|
|
|
|
return xerrors.Errorf("actor execution failed")
|
|
|
|
}
|
|
|
|
|
|
|
|
var result eam.CreateReturn
|
|
|
|
r := bytes.NewReader(wait.Receipt.Return)
|
|
|
|
if err := result.UnmarshalCBOR(r); err != nil {
|
|
|
|
return xerrors.Errorf("error unmarshaling return value: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
addr, err := address.NewIDAddress(result.ActorID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
afmt.Printf("Actor ID: %d\n", result.ActorID)
|
|
|
|
afmt.Printf("ID Address: %s\n", addr)
|
|
|
|
afmt.Printf("Robust Address: %s\n", result.RobustAddress)
|
|
|
|
afmt.Printf("Eth Address: %s\n", "0x"+hex.EncodeToString(result.EthAddress[:]))
|
|
|
|
|
|
|
|
ea, err := ethtypes.CastEthAddress(result.EthAddress[:])
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to create ethereum address: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
delegated, err := ea.ToFilecoinAddress()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to calculate f4 address: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
afmt.Printf("f4 Address: %s\n", delegated)
|
|
|
|
|
|
|
|
if len(wait.Receipt.Return) > 0 {
|
|
|
|
result := base64.StdEncoding.EncodeToString(wait.Receipt.Return)
|
|
|
|
afmt.Printf("Return: %s\n", result)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var EvmInvokeCmd = &cli.Command{
|
|
|
|
Name: "invoke",
|
|
|
|
Usage: "Invoke an EVM smart contract using the specified CALLDATA",
|
|
|
|
ArgsUsage: "address calldata",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "from",
|
|
|
|
Usage: "optionally specify the account to use for sending the exec message",
|
|
|
|
}, &cli.IntFlag{
|
|
|
|
Name: "value",
|
|
|
|
Usage: "optionally specify the value to be sent with the invokation message",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
afmt := NewAppFmt(cctx.App)
|
|
|
|
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
2023-01-19 16:41:11 +00:00
|
|
|
if argc := cctx.Args().Len(); argc != 2 {
|
|
|
|
return xerrors.Errorf("must pass the address and calldata")
|
2023-01-13 19:11:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
addr, err := address.NewFromString(cctx.Args().Get(0))
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to decode address: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var calldata []byte
|
2023-02-09 17:49:08 +00:00
|
|
|
calldata, err = ethtypes.DecodeHexStringTrimSpace(cctx.Args().Get(1))
|
2023-01-13 19:11:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("decoding hex input data: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var buffer bytes.Buffer
|
|
|
|
if err := cbg.WriteByteArray(&buffer, calldata); err != nil {
|
|
|
|
return xerrors.Errorf("failed to encode evm params as cbor: %w", err)
|
|
|
|
}
|
|
|
|
calldata = buffer.Bytes()
|
|
|
|
|
|
|
|
var fromAddr address.Address
|
|
|
|
if from := cctx.String("from"); from == "" {
|
|
|
|
defaddr, err := api.WalletDefaultAddress(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fromAddr = defaddr
|
|
|
|
} else {
|
|
|
|
addr, err := address.NewFromString(from)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fromAddr = addr
|
|
|
|
}
|
|
|
|
|
|
|
|
val := abi.NewTokenAmount(cctx.Int64("value"))
|
|
|
|
msg := &types.Message{
|
|
|
|
To: addr,
|
|
|
|
From: fromAddr,
|
|
|
|
Value: val,
|
2023-01-19 16:41:11 +00:00
|
|
|
Method: builtintypes.MethodsEVM.InvokeContract,
|
2023-01-13 19:11:13 +00:00
|
|
|
Params: calldata,
|
|
|
|
}
|
|
|
|
|
|
|
|
afmt.Println("sending message...")
|
|
|
|
smsg, err := api.MpoolPushMessage(ctx, msg, nil)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to push message: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
afmt.Println("waiting for message to execute...")
|
|
|
|
wait, err := api.StateWaitMsg(ctx, smsg.Cid(), 0)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("error waiting for message: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check it executed successfully
|
|
|
|
if wait.Receipt.ExitCode != 0 {
|
|
|
|
return xerrors.Errorf("actor execution failed")
|
|
|
|
}
|
|
|
|
|
|
|
|
afmt.Println("Gas used: ", wait.Receipt.GasUsed)
|
|
|
|
result, err := cbg.ReadByteArray(bytes.NewBuffer(wait.Receipt.Return), uint64(len(wait.Receipt.Return)))
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("evm result not correctly encoded: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(result) > 0 {
|
|
|
|
afmt.Println(hex.EncodeToString(result))
|
|
|
|
} else {
|
|
|
|
afmt.Println("OK")
|
|
|
|
}
|
|
|
|
|
|
|
|
if eventsRoot := wait.Receipt.EventsRoot; eventsRoot != nil {
|
|
|
|
afmt.Println("Events emitted:")
|
|
|
|
|
|
|
|
s := &apiIpldStore{ctx, api}
|
|
|
|
amt, err := amt4.LoadAMT(ctx, s, *eventsRoot, amt4.UseTreeBitWidth(types.EventAMTBitwidth))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var evt types.Event
|
|
|
|
err = amt.ForEach(ctx, func(u uint64, deferred *cbg.Deferred) error {
|
|
|
|
fmt.Printf("%x\n", deferred.Raw)
|
|
|
|
if err := evt.UnmarshalCBOR(bytes.NewReader(deferred.Raw)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Printf("\tEmitter ID: %s\n", evt.Emitter)
|
|
|
|
for _, e := range evt.Entries {
|
|
|
|
value, err := cbg.ReadByteArray(bytes.NewBuffer(e.Value), uint64(len(e.Value)))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Printf("\t\tKey: %s, Value: 0x%x, Flags: b%b\n", e.Key, value, e.Flags)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
func ethAddrFromFilecoinAddress(ctx context.Context, addr address.Address, fnapi v0api.FullNode) (ethtypes.EthAddress, address.Address, error) {
|
|
|
|
var faddr address.Address
|
|
|
|
var err error
|
|
|
|
|
|
|
|
switch addr.Protocol() {
|
|
|
|
case address.BLS, address.SECP256K1:
|
|
|
|
faddr, err = fnapi.StateLookupID(ctx, addr, types.EmptyTSK)
|
|
|
|
if err != nil {
|
|
|
|
return ethtypes.EthAddress{}, addr, err
|
|
|
|
}
|
|
|
|
case address.Actor, address.ID:
|
|
|
|
faddr, err = fnapi.StateLookupID(ctx, addr, types.EmptyTSK)
|
|
|
|
if err != nil {
|
|
|
|
return ethtypes.EthAddress{}, addr, err
|
|
|
|
}
|
|
|
|
fAct, err := fnapi.StateGetActor(ctx, faddr, types.EmptyTSK)
|
|
|
|
if err != nil {
|
|
|
|
return ethtypes.EthAddress{}, addr, err
|
|
|
|
}
|
|
|
|
if fAct.Address != nil && (*fAct.Address).Protocol() == address.Delegated {
|
|
|
|
faddr = *fAct.Address
|
|
|
|
}
|
|
|
|
case address.Delegated:
|
|
|
|
faddr = addr
|
|
|
|
default:
|
|
|
|
return ethtypes.EthAddress{}, addr, xerrors.Errorf("Filecoin address doesn't match known protocols")
|
|
|
|
}
|
|
|
|
|
|
|
|
ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(faddr)
|
|
|
|
if err != nil {
|
|
|
|
return ethtypes.EthAddress{}, addr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ethAddr, faddr, nil
|
|
|
|
}
|