|
|
|
@ -2,7 +2,7 @@ package main |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"crypto/ecdsa" |
|
|
|
|
"crypto/rand" |
|
|
|
|
cRand "crypto/rand" |
|
|
|
|
"encoding/base64" |
|
|
|
|
"encoding/hex" |
|
|
|
|
"flag" |
|
|
|
@ -10,6 +10,7 @@ import ( |
|
|
|
|
"io" |
|
|
|
|
"io/ioutil" |
|
|
|
|
"math/big" |
|
|
|
|
"math/rand" |
|
|
|
|
"os" |
|
|
|
|
"path" |
|
|
|
|
"strconv" |
|
|
|
@ -20,10 +21,16 @@ import ( |
|
|
|
|
|
|
|
|
|
"github.com/ethereum/go-ethereum/log" |
|
|
|
|
"github.com/ethereum/go-ethereum/params" |
|
|
|
|
"github.com/harmony-one/harmony/api/client" |
|
|
|
|
clientService "github.com/harmony-one/harmony/api/client/service" |
|
|
|
|
proto_node "github.com/harmony-one/harmony/api/proto/node" |
|
|
|
|
"github.com/harmony-one/harmony/core/types" |
|
|
|
|
"github.com/harmony-one/harmony/internal/wallet/wallet" |
|
|
|
|
nodeconfig "github.com/harmony-one/harmony/internal/configs/node" |
|
|
|
|
"github.com/harmony-one/harmony/internal/utils" |
|
|
|
|
"github.com/harmony-one/harmony/node" |
|
|
|
|
"github.com/harmony-one/harmony/p2p" |
|
|
|
|
p2p_host "github.com/harmony-one/harmony/p2p/host" |
|
|
|
|
"github.com/harmony-one/harmony/p2p/p2pimpl" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
var ( |
|
|
|
@ -44,6 +51,10 @@ type AccountState struct { |
|
|
|
|
nonce uint64 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
rpcRetry = 3 |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
var ( |
|
|
|
|
// Account subcommands
|
|
|
|
|
accountImportCommand = flag.NewFlagSet("import", flag.ExitOnError) |
|
|
|
@ -54,7 +65,7 @@ var ( |
|
|
|
|
transferSenderPtr = transferCommand.String("from", "0", "Specify the sender account address or index") |
|
|
|
|
transferReceiverPtr = transferCommand.String("to", "", "Specify the receiver account") |
|
|
|
|
transferAmountPtr = transferCommand.Float64("amount", 0, "Specify the amount to transfer") |
|
|
|
|
transferShardIDPtr = transferCommand.Int("shardID", -1, "Specify the shard ID for the transfer") |
|
|
|
|
transferShardIDPtr = transferCommand.Int("shardID", 0, "Specify the shard ID for the transfer") |
|
|
|
|
transferInputDataPtr = transferCommand.String("inputData", "", "Base64-encoded input data to embed in the transaction") |
|
|
|
|
|
|
|
|
|
freeTokenCommand = flag.NewFlagSet("getFreeToken", flag.ExitOnError) |
|
|
|
@ -62,25 +73,38 @@ var ( |
|
|
|
|
|
|
|
|
|
balanceCommand = flag.NewFlagSet("getFreeToken", flag.ExitOnError) |
|
|
|
|
balanceAddressPtr = balanceCommand.String("address", "", "Specify the account address to check balance for") |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// Quit the program once stopChan received message
|
|
|
|
|
stopChan = make(chan struct{}) |
|
|
|
|
// Print out progress char
|
|
|
|
|
tick = time.NewTicker(time.Second) |
|
|
|
|
// Flag to keep waiting for async call finished
|
|
|
|
|
async = true |
|
|
|
|
var ( |
|
|
|
|
// list of bootnodes
|
|
|
|
|
addrStrings = []string{"/ip4/100.26.90.187/tcp/9876/p2p/QmZJJx6AdaoEkGLrYG4JeLCKeCKDjnFz2wfHNHxAqFSGA9", "/ip4/54.213.43.194/tcp/9876/p2p/QmQayinFSgMMw5cSpDUiD9pQ2WeP6WNmGxpZ6ou3mdVFJX"} |
|
|
|
|
|
|
|
|
|
// list of rpc servers
|
|
|
|
|
// TODO; (leo) take build time parameters or environment parameters to add rpcServers
|
|
|
|
|
// Then this can be automated
|
|
|
|
|
rpcServers = []struct { |
|
|
|
|
IP string |
|
|
|
|
Port string |
|
|
|
|
}{ |
|
|
|
|
{"18.236.96.207", "14555"}, |
|
|
|
|
{"18.206.91.170", "14555"}, |
|
|
|
|
{"54.213.63.26", "14555"}, |
|
|
|
|
{"13.229.96.10", "14555"}, |
|
|
|
|
{"34.243.2.56", "14555"}, |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// setupLog setup log for verbose output
|
|
|
|
|
func setupLog() { |
|
|
|
|
// enable logging for wallet
|
|
|
|
|
h := log.StreamHandler(os.Stdout, log.TerminalFormat(false)) |
|
|
|
|
h := log.StreamHandler(os.Stdout, log.TerminalFormat(true)) |
|
|
|
|
log.Root().SetHandler(h) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// The main wallet program entrance. Note the this wallet program is for demo-purpose only. It does not implement
|
|
|
|
|
// the secure storage of keys.
|
|
|
|
|
func main() { |
|
|
|
|
rand.Seed(int64(time.Now().Nanosecond())) |
|
|
|
|
|
|
|
|
|
// Verify that a subcommand has been provided
|
|
|
|
|
// os.Arg[0] is the main command
|
|
|
|
@ -118,45 +142,60 @@ func main() { |
|
|
|
|
case "-version": |
|
|
|
|
printVersion(os.Args[0]) |
|
|
|
|
case "new": |
|
|
|
|
go processNewCommnad() |
|
|
|
|
processNewCommnad() |
|
|
|
|
case "list": |
|
|
|
|
go processListCommand() |
|
|
|
|
processListCommand() |
|
|
|
|
case "removeAll": |
|
|
|
|
go clearKeystore() |
|
|
|
|
fmt.Println("All existing accounts deleted...") |
|
|
|
|
clearKeystore() |
|
|
|
|
case "import": |
|
|
|
|
go processImportCommnad() |
|
|
|
|
processImportCommnad() |
|
|
|
|
case "balances": |
|
|
|
|
go processBalancesCommand() |
|
|
|
|
processBalancesCommand() |
|
|
|
|
case "getFreeToken": |
|
|
|
|
go processGetFreeToken() |
|
|
|
|
processGetFreeToken() |
|
|
|
|
case "transfer": |
|
|
|
|
go processTransferCommand() |
|
|
|
|
processTransferCommand() |
|
|
|
|
default: |
|
|
|
|
fmt.Printf("Unknown action: %s\n", os.Args[1]) |
|
|
|
|
flag.PrintDefaults() |
|
|
|
|
os.Exit(1) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Waiting for async call finished and print out some progress
|
|
|
|
|
for async { |
|
|
|
|
select { |
|
|
|
|
case <-tick.C: |
|
|
|
|
fmt.Printf("=") |
|
|
|
|
case <-stopChan: |
|
|
|
|
fmt.Println("Done.") |
|
|
|
|
os.Exit(0) |
|
|
|
|
} |
|
|
|
|
// createWalletNode creates wallet server node.
|
|
|
|
|
func createWalletNode() *node.Node { |
|
|
|
|
bootNodeAddrs, err := utils.StringsToAddrs(addrStrings) |
|
|
|
|
if err != nil { |
|
|
|
|
panic(err) |
|
|
|
|
} |
|
|
|
|
utils.BootNodes = bootNodeAddrs |
|
|
|
|
|
|
|
|
|
shardIDs := []uint32{0} |
|
|
|
|
|
|
|
|
|
// dummy host for wallet
|
|
|
|
|
// TODO: potentially, too many dummy IP may flush out good IP address from our bootnode DHT
|
|
|
|
|
// we need to understand the impact to bootnode DHT with this dummy host ip added
|
|
|
|
|
self := p2p.Peer{IP: "127.0.0.1", Port: "6999"} |
|
|
|
|
priKey, _, _ := utils.GenKeyP2P("127.0.0.1", "6999") |
|
|
|
|
host, err := p2pimpl.NewHost(&self, priKey) |
|
|
|
|
if err != nil { |
|
|
|
|
panic(err) |
|
|
|
|
} |
|
|
|
|
w := node.New(host, nil, nil) |
|
|
|
|
w.Client = client.NewClient(w.GetHost(), shardIDs) |
|
|
|
|
|
|
|
|
|
w.NodeConfig.SetRole(nodeconfig.ClientNode) |
|
|
|
|
w.ServiceManagerSetup() |
|
|
|
|
w.RunServices() |
|
|
|
|
return w |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func processNewCommnad() { |
|
|
|
|
randomBytes := [32]byte{} |
|
|
|
|
_, err := io.ReadFull(rand.Reader, randomBytes[:]) |
|
|
|
|
_, err := io.ReadFull(cRand.Reader, randomBytes[:]) |
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
fmt.Println("Failed to get randomness for the private key...") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
priKey, err := crypto2.GenerateKey() |
|
|
|
@ -166,7 +205,6 @@ func processNewCommnad() { |
|
|
|
|
storePrivateKey(crypto2.FromECDSA(priKey)) |
|
|
|
|
fmt.Printf("New account created with address:{%s}\n", crypto2.PubkeyToAddress(priKey.PublicKey).Hex()) |
|
|
|
|
fmt.Printf("Please keep a copy of the private key:{%s}\n", hex.EncodeToString(crypto2.FromECDSA(priKey))) |
|
|
|
|
async = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func processListCommand() { |
|
|
|
@ -174,7 +212,6 @@ func processListCommand() { |
|
|
|
|
fmt.Printf("Account %d:{%s}\n", i, crypto2.PubkeyToAddress(key.PublicKey).Hex()) |
|
|
|
|
fmt.Printf(" PrivateKey:{%s}\n", hex.EncodeToString(key.D.Bytes())) |
|
|
|
|
} |
|
|
|
|
async = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func processImportCommnad() { |
|
|
|
@ -182,7 +219,6 @@ func processImportCommnad() { |
|
|
|
|
priKey := *accountImportPtr |
|
|
|
|
if priKey == "" { |
|
|
|
|
fmt.Println("Error: --privateKey is required") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if !accountImportCommand.Parsed() { |
|
|
|
@ -194,50 +230,43 @@ func processImportCommnad() { |
|
|
|
|
} |
|
|
|
|
storePrivateKey(priKeyBytes) |
|
|
|
|
fmt.Println("Private key imported...") |
|
|
|
|
async = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func processBalancesCommand() { |
|
|
|
|
balanceCommand.Parse(os.Args[2:]) |
|
|
|
|
walletNode := wallet.CreateWalletNode() |
|
|
|
|
|
|
|
|
|
if *balanceAddressPtr == "" { |
|
|
|
|
for i, address := range ReadAddresses() { |
|
|
|
|
fmt.Printf("Account %d: %s:\n", i, address.Hex()) |
|
|
|
|
for shardID, balanceNonce := range FetchBalance(address, walletNode) { |
|
|
|
|
for shardID, balanceNonce := range FetchBalance(address) { |
|
|
|
|
fmt.Printf(" Balance in Shard %d: %s, nonce: %v \n", shardID, convertBalanceIntoReadableFormat(balanceNonce.balance), balanceNonce.nonce) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
address := common.HexToAddress(*balanceAddressPtr) |
|
|
|
|
fmt.Printf("Account: %s:\n", address.Hex()) |
|
|
|
|
for shardID, balanceNonce := range FetchBalance(address, walletNode) { |
|
|
|
|
for shardID, balanceNonce := range FetchBalance(address) { |
|
|
|
|
fmt.Printf(" Balance in Shard %d: %s, nonce: %v \n", shardID, convertBalanceIntoReadableFormat(balanceNonce.balance), balanceNonce.nonce) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
async = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func processGetFreeToken() { |
|
|
|
|
freeTokenCommand.Parse(os.Args[2:]) |
|
|
|
|
walletNode := wallet.CreateWalletNode() |
|
|
|
|
|
|
|
|
|
if *freeTokenAddressPtr == "" { |
|
|
|
|
fmt.Println("Error: --address is required") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} else { |
|
|
|
|
address := common.HexToAddress(*freeTokenAddressPtr) |
|
|
|
|
GetFreeToken(address) |
|
|
|
|
} |
|
|
|
|
address := common.HexToAddress(*freeTokenAddressPtr) |
|
|
|
|
|
|
|
|
|
GetFreeToken(address, walletNode) |
|
|
|
|
|
|
|
|
|
async = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func processTransferCommand() { |
|
|
|
|
transferCommand.Parse(os.Args[2:]) |
|
|
|
|
if !transferCommand.Parsed() { |
|
|
|
|
fmt.Println("Failed to parse flags") |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
sender := *transferSenderPtr |
|
|
|
|
receiver := *transferReceiverPtr |
|
|
|
@ -249,24 +278,20 @@ func processTransferCommand() { |
|
|
|
|
if err != nil { |
|
|
|
|
fmt.Printf("Cannot base64-decode input data (%s): %s\n", |
|
|
|
|
base64InputData, err) |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if shardID == -1 { |
|
|
|
|
fmt.Println("Please specify the shard ID for the transfer (e.g. --shardID=0)") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if amount <= 0 { |
|
|
|
|
fmt.Println("Please specify positive amount to transfer") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
priKeys := readPrivateKeys() |
|
|
|
|
if len(priKeys) == 0 { |
|
|
|
|
fmt.Println("No imported account to use.") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
senderIndex, err := strconv.Atoi(sender) |
|
|
|
@ -281,48 +306,45 @@ func processTransferCommand() { |
|
|
|
|
} |
|
|
|
|
if senderIndex == -1 { |
|
|
|
|
fmt.Println("The specified sender account does not exist in the wallet.") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if senderIndex >= len(priKeys) { |
|
|
|
|
fmt.Println("Sender account index out of bounds.") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
receiverAddress := common.HexToAddress(receiver) |
|
|
|
|
if len(receiverAddress) != 20 { |
|
|
|
|
fmt.Println("The receiver address is not valid.") |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Generate transaction
|
|
|
|
|
senderPriKey := priKeys[senderIndex] |
|
|
|
|
senderAddress := addresses[senderIndex] |
|
|
|
|
walletNode := wallet.CreateWalletNode() |
|
|
|
|
shardIDToAccountState := FetchBalance(senderAddress, walletNode) |
|
|
|
|
|
|
|
|
|
walletNode := createWalletNode() |
|
|
|
|
|
|
|
|
|
shardIDToAccountState := FetchBalance(senderAddress) |
|
|
|
|
|
|
|
|
|
state, ok := shardIDToAccountState[uint32(shardID)] |
|
|
|
|
if !ok { |
|
|
|
|
fmt.Printf("Failed connecting to the shard %d\n", shardID) |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
balance := state.balance |
|
|
|
|
balance = balance.Div(balance, big.NewInt(params.GWei)) |
|
|
|
|
if amount > float64(balance.Uint64())/params.GWei { |
|
|
|
|
fmt.Printf("Balance is not enough for the transfer, current balance is %.6f\n", float64(balance.Uint64())/params.GWei) |
|
|
|
|
async = false |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
amountBigInt := big.NewInt(int64(amount * params.GWei)) |
|
|
|
|
amountBigInt = amountBigInt.Mul(amountBigInt, big.NewInt(params.GWei)) |
|
|
|
|
tx, _ := types.SignTx(types.NewTransaction(state.nonce, receiverAddress, uint32(shardID), amountBigInt, params.TxGas, nil, inputData), types.HomesteadSigner{}, senderPriKey) |
|
|
|
|
wallet.SubmitTransaction(tx, walletNode, uint32(shardID), stopChan) |
|
|
|
|
submitTransaction(tx, walletNode, uint32(shardID)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func convertBalanceIntoReadableFormat(balance *big.Int) string { |
|
|
|
@ -366,39 +388,54 @@ func convertBalanceIntoReadableFormat(balance *big.Int) string { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// FetchBalance fetches account balance of specified address from the Harmony network
|
|
|
|
|
// TODO add support for non beacon chain shards
|
|
|
|
|
func FetchBalance(address common.Address, walletNode *node.Node) map[uint32]AccountState { |
|
|
|
|
// TODO: (chao) add support for non beacon chain shards
|
|
|
|
|
func FetchBalance(address common.Address) map[uint32]AccountState { |
|
|
|
|
result := make(map[uint32]AccountState) |
|
|
|
|
peers := wallet.GetPeersFromBeaconChain(walletNode) |
|
|
|
|
if len(peers) == 0 { |
|
|
|
|
fmt.Printf("[FATAL] Can't find peers\n") |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
peer := peers[0] |
|
|
|
|
port, _ := strconv.Atoi(peer.Port) |
|
|
|
|
client := clientService.NewClient(peer.IP, strconv.Itoa(port+node.ClientServicePortDiff)) |
|
|
|
|
response := client.GetBalance(address) |
|
|
|
|
balance := big.NewInt(0) |
|
|
|
|
balance.SetBytes(response.Balance) |
|
|
|
|
result[0] = AccountState{balance, response.Nonce} |
|
|
|
|
result[0] = AccountState{balance, 0} |
|
|
|
|
|
|
|
|
|
for retry := 0; retry < rpcRetry; retry++ { |
|
|
|
|
server := rpcServers[rand.Intn(len(rpcServers))] |
|
|
|
|
client, err := clientService.NewClient(server.IP, server.Port) |
|
|
|
|
if err != nil { |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
log.Debug("FetchBalance", "server", server) |
|
|
|
|
response, err := client.GetBalance(address) |
|
|
|
|
if err != nil { |
|
|
|
|
log.Info("failed to get balance, retrying ...") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
log.Debug("FetchBalance", "response", response) |
|
|
|
|
balance.SetBytes(response.Balance) |
|
|
|
|
result[0] = AccountState{balance, response.Nonce} |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
return result |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// GetFreeToken requests for token test token on each shard
|
|
|
|
|
func GetFreeToken(address common.Address, walletNode *node.Node) { |
|
|
|
|
peers := wallet.GetPeersFromBeaconChain(walletNode) |
|
|
|
|
if len(peers) == 0 { |
|
|
|
|
fmt.Printf("[FATAL] Can't find peers\n") |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
peer := peers[0] |
|
|
|
|
port, _ := strconv.Atoi(peer.Port) |
|
|
|
|
client := clientService.NewClient(peer.IP, strconv.Itoa(port+node.ClientServicePortDiff)) |
|
|
|
|
response := client.GetFreeToken(address) |
|
|
|
|
func GetFreeToken(address common.Address) { |
|
|
|
|
for retry := 0; retry < rpcRetry; retry++ { |
|
|
|
|
server := rpcServers[0] |
|
|
|
|
client, err := clientService.NewClient(server.IP, server.Port) |
|
|
|
|
if err != nil { |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
txID := common.Hash{} |
|
|
|
|
txID.SetBytes(response.TxId) |
|
|
|
|
fmt.Printf("Transaction Id requesting free token in shard %d: %s\n", int(0), txID.Hex()) |
|
|
|
|
log.Debug("GetFreeToken", "server", server) |
|
|
|
|
response, err := client.GetFreeToken(address) |
|
|
|
|
if err != nil { |
|
|
|
|
log.Info("failed to get free token, retrying ...") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
log.Debug("GetFreeToken", "response", response) |
|
|
|
|
txID := common.Hash{} |
|
|
|
|
txID.SetBytes(response.TxId) |
|
|
|
|
fmt.Printf("Transaction Id requesting free token in shard %d: %s\n", int(0), txID.Hex()) |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ReadAddresses reads the addresses stored in local keystore
|
|
|
|
@ -439,6 +476,7 @@ func storePrivateKey(priKey []byte) { |
|
|
|
|
// clearKeystore deletes all data in the local keystore
|
|
|
|
|
func clearKeystore() { |
|
|
|
|
ioutil.WriteFile("keystore", []byte{}, 0644) |
|
|
|
|
fmt.Println("All existing accounts deleted...") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// readPrivateKeys reads all the private key stored in local keystore
|
|
|
|
@ -458,3 +496,18 @@ func readPrivateKeys() []*ecdsa.PrivateKey { |
|
|
|
|
} |
|
|
|
|
return priKeys |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// submitTransaction submits the transaction to the Harmony network
|
|
|
|
|
func submitTransaction(tx *types.Transaction, walletNode *node.Node, shardID uint32) error { |
|
|
|
|
msg := proto_node.ConstructTransactionListMessageAccount(types.Transactions{tx}) |
|
|
|
|
err := walletNode.GetHost().SendMessageToGroups([]p2p.GroupID{p2p.GroupIDBeaconClient}, p2p_host.ConstructP2pMessage(byte(0), msg)) |
|
|
|
|
if err != nil { |
|
|
|
|
fmt.Printf("Error in SubmitTransaction: %v\n", err) |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
fmt.Printf("Transaction Id for shard %d: %s\n", int(shardID), tx.Hash().Hex()) |
|
|
|
|
// FIXME (leo): how to we know the tx was successful sent to the network
|
|
|
|
|
// this is a hacky way to wait for sometime
|
|
|
|
|
time.Sleep(2 * time.Second) |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|