mirror of
https://github.com/aljazceru/ark.git
synced 2026-02-19 09:54:20 +01:00
Scaffold noah cli (#10)
* CLI skeleton * noah CLI: send flags * add cypher.go file * fix .PHONY * add password_hash in state.json * encode public key using common pkg * use common.DecodeUrl * remove cli.Exit calls * redeem command: make --amount flag optional only if --force is not set * remove validateURL func * chmod +x scripts/build-noah * Update cmd/noah/redeem.go Co-authored-by: João Bordalo <bordalix@users.noreply.github.com> * Update cmd/noah/redeem.go Co-authored-by: João Bordalo <bordalix@users.noreply.github.com> * Update cmd/noah/init.go Co-authored-by: João Bordalo <bordalix@users.noreply.github.com> * Update cmd/noah/main.go Co-authored-by: João Bordalo <bordalix@users.noreply.github.com> * Update cmd/noah/send.go Co-authored-by: João Bordalo <bordalix@users.noreply.github.com> * rework receive and send * Update cmd/noah/send.go Co-authored-by: João Bordalo <bordalix@users.noreply.github.com> * Update cmd/noah/send.go Co-authored-by: João Bordalo <bordalix@users.noreply.github.com> * Update cmd/noah/redeem.go Co-authored-by: João Bordalo <bordalix@users.noreply.github.com> * receive command: return ark address --------- Co-authored-by: bordalix <joao.bordalo@gmail.com> Co-authored-by: João Bordalo <bordalix@users.noreply.github.com>
This commit is contained in:
18
cmd/noah/balance.go
Normal file
18
cmd/noah/balance.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var balanceCommand = cli.Command{
|
||||
Name: "balance",
|
||||
Usage: "Print balance of the Noah wallet",
|
||||
Action: balanceAction,
|
||||
}
|
||||
|
||||
func balanceAction(ctx *cli.Context) error {
|
||||
fmt.Println("balance is not implemented yet")
|
||||
return nil
|
||||
}
|
||||
114
cmd/noah/common.go
Normal file
114
cmd/noah/common.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func hashPassword(password []byte) []byte {
|
||||
hash := sha256.Sum256(password)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func verifyPassword(password []byte) error {
|
||||
state, err := getState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passwordHashString, ok := state["password_hash"]
|
||||
if !ok {
|
||||
return fmt.Errorf("password hash not found")
|
||||
}
|
||||
|
||||
passwordHash, err := hex.DecodeString(passwordHashString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentPassHash := hashPassword(password)
|
||||
|
||||
if !bytes.Equal(passwordHash, currentPassHash) {
|
||||
return fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPassword() ([]byte, error) {
|
||||
fmt.Print("password: ")
|
||||
passwordInput, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println() // new line
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = verifyPassword(passwordInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return passwordInput, nil
|
||||
}
|
||||
|
||||
func privateKeyFromPassword() (*secp256k1.PrivateKey, error) {
|
||||
state, err := getState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encryptedPrivateKeyString, ok := state["encrypted_private_key"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("encrypted private key not found")
|
||||
}
|
||||
|
||||
encryptedPrivateKey, err := hex.DecodeString(encryptedPrivateKeyString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
password, err := readPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cypher := NewAES128Cypher()
|
||||
privateKeyBytes, err := cypher.Decrypt(encryptedPrivateKey, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey := secp256k1.PrivKeyFromBytes(privateKeyBytes)
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func getServiceProviderPublicKey() (*secp256k1.PublicKey, error) {
|
||||
state, err := getState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arkURL, ok := state["ark_url"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ark url not found")
|
||||
}
|
||||
|
||||
arkPubKey, _, err := common.DecodeUrl(arkURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, publicKey, err := common.DecodePubKey(arkPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return publicKey, nil
|
||||
}
|
||||
54
cmd/noah/config.go
Normal file
54
cmd/noah/config.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var configCommand = cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Print local configuration of the Noah CLI",
|
||||
Action: printConfigAction,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "connect",
|
||||
Usage: "connect <ARK_URL>",
|
||||
Action: connectAction,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func printConfigAction(ctx *cli.Context) error {
|
||||
state, err := getState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, value := range state {
|
||||
fmt.Println(key + ": " + value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func connectAction(ctx *cli.Context) error {
|
||||
if ctx.NArg() != 1 {
|
||||
return fmt.Errorf("missing ark URL")
|
||||
}
|
||||
|
||||
url := ctx.Args().Get(0)
|
||||
|
||||
_, _, err := common.DecodeUrl(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setState(map[string]string{"ark_url": url}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Connected to " + url)
|
||||
return nil
|
||||
}
|
||||
106
cmd/noah/cypher.go
Normal file
106
cmd/noah/cypher.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
type Cypher struct{}
|
||||
|
||||
func NewAES128Cypher() *Cypher {
|
||||
return &Cypher{}
|
||||
}
|
||||
|
||||
func (c *Cypher) Encrypt(privateKey, password []byte) ([]byte, error) {
|
||||
// Due to https://github.com/golang/go/issues/7168.
|
||||
// This call makes sure that memory is freed in case the GC doesn't do that
|
||||
// right after the encryption/decryption.
|
||||
defer debug.FreeOSMemory()
|
||||
|
||||
if len(privateKey) == 0 {
|
||||
return nil, fmt.Errorf("missing plaintext private key")
|
||||
}
|
||||
if len(password) == 0 {
|
||||
return nil, fmt.Errorf("missing encryption password")
|
||||
}
|
||||
|
||||
key, salt, err := deriveKey(password, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockCipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(blockCipher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, privateKey, nil)
|
||||
ciphertext = append(ciphertext, salt...)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func (c *Cypher) Decrypt(encrypted, password []byte) ([]byte, error) {
|
||||
defer debug.FreeOSMemory()
|
||||
|
||||
if len(encrypted) == 0 {
|
||||
return nil, fmt.Errorf("missing encrypted mnemonic")
|
||||
}
|
||||
if len(password) == 0 {
|
||||
return nil, fmt.Errorf("missing decryption password")
|
||||
}
|
||||
|
||||
salt := encrypted[len(encrypted)-32:]
|
||||
data := encrypted[:len(encrypted)-32]
|
||||
|
||||
key, _, err := deriveKey(password, salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockCipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(blockCipher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce, text := data[:gcm.NonceSize()], data[gcm.NonceSize():]
|
||||
plaintext, err := gcm.Open(nil, nonce, text, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid password")
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// deriveKey derives a 32 byte array key from a custom passhprase
|
||||
func deriveKey(password, salt []byte) ([]byte, []byte, error) {
|
||||
if salt == nil {
|
||||
salt = make([]byte, 32)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
// 2^20 = 1048576 recommended length for key-stretching
|
||||
// check the doc for other recommended values:
|
||||
// https://godoc.org/golang.org/x/crypto/scrypt
|
||||
key, err := scrypt.Key(password, salt, 1048576, 8, 1, 32)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return key, salt, nil
|
||||
}
|
||||
90
cmd/noah/init.go
Normal file
90
cmd/noah/init.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
passwordFlag = cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "password to encrypt private key",
|
||||
Value: "",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
privateKeyFlag = cli.StringFlag{
|
||||
Name: "prvkey",
|
||||
Usage: "optional, private key to encrypt",
|
||||
Value: "",
|
||||
Required: false,
|
||||
}
|
||||
)
|
||||
|
||||
var initCommand = cli.Command{
|
||||
Name: "init",
|
||||
Usage: "Initialize Noah wallet private key, encrypted with password",
|
||||
Action: initAction,
|
||||
Flags: []cli.Flag{
|
||||
&passwordFlag,
|
||||
&privateKeyFlag,
|
||||
},
|
||||
}
|
||||
|
||||
func initAction(ctx *cli.Context) error {
|
||||
privateKeyString := ctx.String("prvkey")
|
||||
password := ctx.String("password")
|
||||
|
||||
if len(password) <= 0 {
|
||||
return fmt.Errorf("missing password flag (--password)")
|
||||
}
|
||||
|
||||
var privateKey *secp256k1.PrivateKey
|
||||
|
||||
if len(privateKeyString) <= 0 {
|
||||
privKey, err := generateRandomPrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey = privKey
|
||||
} else {
|
||||
privKeyBytes, err := hex.DecodeString(privateKeyString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateKey = secp256k1.PrivKeyFromBytes(privKeyBytes)
|
||||
}
|
||||
|
||||
cypher := NewAES128Cypher()
|
||||
|
||||
encryptedPrivateKey, err := cypher.Encrypt(privateKey.Serialize(), []byte(password))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passwordHash := hashPassword([]byte(password))
|
||||
|
||||
state := map[string]string{
|
||||
"encrypted_private_key": hex.EncodeToString(encryptedPrivateKey),
|
||||
"password_hash": hex.EncodeToString(passwordHash),
|
||||
}
|
||||
|
||||
if err := setState(state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateRandomPrivateKey() (*secp256k1.PrivateKey, error) {
|
||||
privKey, err := btcec.NewPrivateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
}
|
||||
158
cmd/noah/main.go
Normal file
158
cmd/noah/main.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
DATADIR_ENVVAR = "NOAH_DATADIR"
|
||||
STATE_FILE = "state.json"
|
||||
defaultArkURL = "ark://apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x?relays=arelay1qt6f8p7h5f6tm7fv2z5wg92sz92rn9desfhd5733se4lkrptqtdrq65987l-arelay1qt6f8p7h5f6tm7fv2z5wg92sz92rn9desfhd5733se4lkrptqtdrq65987l"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "alpha"
|
||||
|
||||
noahDataDirectory = common.AppDataDir("noah", false)
|
||||
statePath = filepath.Join(noahDataDirectory, STATE_FILE)
|
||||
|
||||
initialState = map[string]string{
|
||||
"ark_url": defaultArkURL,
|
||||
"encrypted_private_key": "",
|
||||
"password_hash": "",
|
||||
}
|
||||
)
|
||||
|
||||
func initCLIEnv() {
|
||||
dataDir := cleanAndExpandPath(os.Getenv(DATADIR_ENVVAR))
|
||||
if len(dataDir) <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
noahDataDirectory = dataDir
|
||||
statePath = filepath.Join(noahDataDirectory, STATE_FILE)
|
||||
}
|
||||
|
||||
func main() {
|
||||
initCLIEnv()
|
||||
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Version = version
|
||||
app.Name = "noah CLI"
|
||||
app.Usage = "Command line interface for Ark wallet"
|
||||
app.Commands = append(
|
||||
app.Commands,
|
||||
&balanceCommand,
|
||||
&configCommand,
|
||||
&initCommand,
|
||||
&receiveCommand,
|
||||
&redeemCommand,
|
||||
&sendCommand,
|
||||
)
|
||||
|
||||
app.Before = func(ctx *cli.Context) error {
|
||||
if _, err := os.Stat(noahDataDirectory); os.IsNotExist(err) {
|
||||
return os.Mkdir(noahDataDirectory, os.ModeDir|0755)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("error: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanAndExpandPath expands environment variables and leading ~ in the
|
||||
// passed path, cleans the result, and returns it.
|
||||
// This function is taken from https://github.com/btcsuite/btcd
|
||||
func cleanAndExpandPath(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Expand initial ~ to OS specific home directory.
|
||||
if strings.HasPrefix(path, "~") {
|
||||
var homeDir string
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
homeDir = u.HomeDir
|
||||
} else {
|
||||
homeDir = os.Getenv("HOME")
|
||||
}
|
||||
|
||||
path = strings.Replace(path, "~", homeDir, 1)
|
||||
}
|
||||
|
||||
// NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%,
|
||||
// but the variables can still be expanded via POSIX-style $VARIABLE.
|
||||
return filepath.Clean(os.ExpandEnv(path))
|
||||
}
|
||||
|
||||
func getState() (map[string]string, error) {
|
||||
file, err := os.ReadFile(statePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if err := setInitialState(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return initialState, nil
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func setInitialState() error {
|
||||
jsonString, err := json.Marshal(initialState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(statePath, jsonString, 0755)
|
||||
}
|
||||
|
||||
func setState(data map[string]string) error {
|
||||
currentData, err := getState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergedData := merge(currentData, data)
|
||||
|
||||
jsonString, err := json.Marshal(mergedData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(statePath, jsonString, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing to file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func merge(maps ...map[string]string) map[string]string {
|
||||
merge := make(map[string]string, 0)
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
merge[k] = v
|
||||
}
|
||||
}
|
||||
return merge
|
||||
}
|
||||
37
cmd/noah/receive.go
Normal file
37
cmd/noah/receive.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var receiveCommand = cli.Command{
|
||||
Name: "receive",
|
||||
Usage: "Print the Ark address associated with your wallet and the connected Ark",
|
||||
Action: receiveAction,
|
||||
}
|
||||
|
||||
func receiveAction(ctx *cli.Context) error {
|
||||
privateKey, err := privateKeyFromPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publicKey := privateKey.PubKey()
|
||||
|
||||
aspPublicKey, err := getServiceProviderPublicKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr, err := common.EncodeAddress(common.MainNet.Addr, publicKey, aspPublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(addr)
|
||||
|
||||
return nil
|
||||
}
|
||||
67
cmd/noah/redeem.go
Normal file
67
cmd/noah/redeem.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
addressFlag = cli.StringFlag{
|
||||
Name: "address",
|
||||
Usage: "main chain address receiving the redeeemed VTXO",
|
||||
Value: "",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
amountToRedeemFlag = cli.Uint64Flag{
|
||||
Name: "amount",
|
||||
Usage: "amount to redeem",
|
||||
Value: 0,
|
||||
Required: false,
|
||||
}
|
||||
|
||||
forceFlag = cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "force redemption without collaborate with the Ark service provider",
|
||||
Value: false,
|
||||
Required: false,
|
||||
}
|
||||
)
|
||||
|
||||
var redeemCommand = cli.Command{
|
||||
Name: "redeem",
|
||||
Usage: "Redeem VTXO(s) to onchain",
|
||||
Flags: []cli.Flag{&addressFlag, &amountToRedeemFlag, &forceFlag},
|
||||
Action: redeemAction,
|
||||
}
|
||||
|
||||
func redeemAction(ctx *cli.Context) error {
|
||||
address := ctx.String("address")
|
||||
amount := ctx.Uint64("amount")
|
||||
force := ctx.Bool("force")
|
||||
|
||||
if len(address) <= 0 {
|
||||
return fmt.Errorf("missing address flag (--address)")
|
||||
}
|
||||
|
||||
if !force && amount <= 0 {
|
||||
return fmt.Errorf("missing amount flag (--amount)")
|
||||
}
|
||||
|
||||
if force {
|
||||
return unilateralRedeem(address)
|
||||
}
|
||||
|
||||
return collaborativeRedeem(address, amount)
|
||||
}
|
||||
|
||||
func collaborativeRedeem(address string, amount uint64) error {
|
||||
fmt.Println("collaborative redeem is not implemented yet")
|
||||
return nil
|
||||
}
|
||||
|
||||
func unilateralRedeem(address string) error {
|
||||
fmt.Println("unilateral redeem is not implemented yet")
|
||||
return nil
|
||||
}
|
||||
60
cmd/noah/send.go
Normal file
60
cmd/noah/send.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
receiversFlag = cli.StringFlag{
|
||||
Name: "receivers",
|
||||
Usage: "receivers of the send transaction, JSON encoded: '[{\"to\": \"<...>\", \"amount\": <...>}, ...]'",
|
||||
Value: "",
|
||||
Required: true,
|
||||
}
|
||||
)
|
||||
|
||||
var sendCommand = cli.Command{
|
||||
Name: "send",
|
||||
Usage: "Send VTXOs to a list of addresses",
|
||||
Action: sendAction,
|
||||
Flags: []cli.Flag{&receiversFlag},
|
||||
}
|
||||
|
||||
func sendAction(ctx *cli.Context) error {
|
||||
receivers := ctx.String("receivers")
|
||||
|
||||
// parse json encoded receivers
|
||||
var receiversJSON []receiverJSON
|
||||
if err := json.Unmarshal([]byte(receivers), &receiversJSON); err != nil {
|
||||
return fmt.Errorf("invalid receivers: %s", err)
|
||||
}
|
||||
|
||||
if len(receiversJSON) <= 0 {
|
||||
return fmt.Errorf("no receivers specified")
|
||||
}
|
||||
|
||||
for _, receiver := range receiversJSON {
|
||||
// TODO: check if receiver asp public key is valid
|
||||
_, _, _, err := common.DecodeAddress(receiver.To)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid receiver address: %s", err)
|
||||
}
|
||||
|
||||
if receiver.Amount <= 0 {
|
||||
return fmt.Errorf("invalid amount: %d", receiver.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("send command is not implemented yet")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type receiverJSON struct {
|
||||
To string `json:"to"`
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
Reference in New Issue
Block a user