package main

import (
	"context"
	"crypto/ecdsa"
	"errors"
	"fmt"
	"log"
	"math/big"
	"os"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/joho/godotenv"
	"github.com/spf13/cobra"
)

var (
	rpcURL    string
	coinAddr  string
	tokenAddr string
	pkHex     string
)

func loadEnv() {
	_ = godotenv.Load("../.env")
	_ = godotenv.Load(".env")

	if rpcURL == "" {
		rpcURL = os.Getenv("RPC_URL")
		if rpcURL == "" {
			rpcURL = os.Getenv("POLYGON_RPC_URL")
			if rpcURL == "" {
				rpcURL = os.Getenv("AMOY_RPC_URL")
			}
		}
	}
	if coinAddr == "" {
		coinAddr = os.Getenv("EASY_COIN_ADDR")
	}
	if tokenAddr == "" {
		tokenAddr = os.Getenv("EASY_TOKEN_ADDR")
	}
	if pkHex == "" {
		pkHex = os.Getenv("PRIVATE_KEY")
		if pkHex == "" {
			pkHex = os.Getenv("EASY_ADMIN_PRIVATE_KEY")
			if pkHex == "" {
				pkHex = os.Getenv("EASY_ADMIM_PRIVATE_KEY") // fallback to example typo
			}
		}
	}
}

func mustClient(ctx context.Context) (*ethclient.Client, *big.Int) {
	if rpcURL == "" {
		log.Fatal("RPC URL not set: use --rpc or set RPC_URL/POLYGON_RPC_URL/AMOY_RPC_URL in .env")
	}
	c, err := ethclient.DialContext(ctx, rpcURL)
	if err != nil {
		log.Fatalf("dial rpc: %v", err)
	}
	chainID, err := c.ChainID(ctx)
	if err != nil {
		log.Fatalf("get chain id: %v", err)
	}
	return c, chainID
}

func mustPrivKey() *ecdsa.PrivateKey {
	if pkHex == "" {
		log.Fatal("Private key not set: use --pk or set PRIVATE_KEY/EASY_ADMIN_PRIVATE_KEY in .env")
	}
	pk, err := crypto.HexToECDSA(strings.TrimPrefix(pkHex, "0x"))
	if err != nil {
		log.Fatalf("invalid private key: %v", err)
	}
	return pk
}

func fromHex32(s string) ([32]byte, error) {
	var out [32]byte
	b := common.FromHex(s)
	if len(b) != 32 {
		return out, fmt.Errorf("expected 32 bytes, got %d", len(b))
	}
	copy(out[:], b)
	return out, nil
}

func roleID(name string) [32]byte {
	upper := strings.ToUpper(name)
	if upper == "DEFAULT_ADMIN_ROLE" || upper == "DEFAULT_ADMIN" || upper == "ADMIN" {
		return [32]byte{}
	}
	// roles are keccak256("<ROLE>"), e.g. "PAUSER_ROLE"
	h := crypto.Keccak256([]byte(upper))
	var out [32]byte
	copy(out[:], h)
	return out
}

func mustAddress(hex string, envName string) common.Address {
	if hex == "" {
		log.Fatalf("missing address: set %s or pass a flag", envName)
	}
	if !common.IsHexAddress(hex) {
		log.Fatalf("invalid address for %s: %s", envName, hex)
	}
	return common.HexToAddress(hex)
}

func toWei(amount string) (*big.Int, error) {
	// parse base-unit integer (no decimals handling here)
	if strings.Contains(amount, ".") {
		return nil, errors.New("amount must be an integer in base units (no decimal point)")
	}
	z := new(big.Int)
	_, ok := z.SetString(amount, 10)
	if !ok {
		return nil, fmt.Errorf("invalid amount: %s", amount)
	}
	return z, nil
}

func boundContract(addr common.Address, abiJSON string, c *ethclient.Client) *bind.BoundContract {
	parsed, err := abi.JSON(strings.NewReader(abiJSON))
	if err != nil {
		log.Fatalf("parse abi: %v", err)
	}
	return bind.NewBoundContract(addr, parsed, c, c, c)
}

func txOpts(ctx context.Context, c *ethclient.Client, chainID *big.Int) *bind.TransactOpts {
	pk := mustPrivKey()
	auth, err := bind.NewKeyedTransactorWithChainID(pk, chainID)
	if err != nil {
		log.Fatalf("transactor: %v", err)
	}
	auth.Context = ctx
	// let node estimate gas; we can set a reasonable timeout
	return auth
}

func callOpts(ctx context.Context) *bind.CallOpts {
	return &bind.CallOpts{Context: ctx}
}

// ABI call helpers compatible with go-ethereum v1.14 BoundContract.Call
func callOut(ctx context.Context, con *bind.BoundContract, method string, args ...interface{}) ([]interface{}, error) {
	var out []interface{}
	if err := con.Call(callOpts(ctx), &out, method, args...); err != nil {
		return nil, err
	}
	return out, nil
}

func callString(ctx context.Context, con *bind.BoundContract, method string, args ...interface{}) (string, error) {
	out, err := callOut(ctx, con, method, args...)
	if err != nil { return "", err }
	v, ok := out[0].(string)
	if !ok { return "", fmt.Errorf("unexpected %T", out[0]) }
	return v, nil
}

func callBool(ctx context.Context, con *bind.BoundContract, method string, args ...interface{}) (bool, error) {
	out, err := callOut(ctx, con, method, args...)
	if err != nil { return false, err }
	v, ok := out[0].(bool)
	if !ok { return false, fmt.Errorf("unexpected %T", out[0]) }
	return v, nil
}

func callUint8(ctx context.Context, con *bind.BoundContract, method string, args ...interface{}) (uint8, error) {
	out, err := callOut(ctx, con, method, args...)
	if err != nil { return 0, err }
	v, ok := out[0].(uint8)
	if !ok { return 0, fmt.Errorf("unexpected %T", out[0]) }
	return v, nil
}

func callBig(ctx context.Context, con *bind.BoundContract, method string, args ...interface{}) (*big.Int, error) {
	out, err := callOut(ctx, con, method, args...)
	if err != nil { return nil, err }
	v, ok := out[0].(*big.Int)
	if !ok { return nil, fmt.Errorf("unexpected %T", out[0]) }
	return v, nil
}

func callAddress(ctx context.Context, con *bind.BoundContract, method string, args ...interface{}) (common.Address, error) {
	out, err := callOut(ctx, con, method, args...)
	if err != nil { return common.Address{}, err }
	v, ok := out[0].(common.Address)
	if !ok { return common.Address{}, fmt.Errorf("unexpected %T", out[0]) }
	return v, nil
}

func callBytes32(ctx context.Context, con *bind.BoundContract, method string, args ...interface{}) ([32]byte, error) {
	out, err := callOut(ctx, con, method, args...)
	if err != nil { return [32]byte{}, err }
	v, ok := out[0].([32]byte)
	if !ok { return [32]byte{}, fmt.Errorf("unexpected %T", out[0]) }
	return v, nil
}

func cmdCoin() *cobra.Command {
	coinCmd := &cobra.Command{
		Use:   "coin",
		Short: "Interact with EasyBRL (ERC20)",
	}

	// info
	coinCmd.AddCommand(&cobra.Command{
		Use:   "info",
		Short: "Show ERC20 name, symbol, decimals, totalSupply, paused",
		Run: func(cmd *cobra.Command, args []string) {
			loadEnv()
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
			defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			name, err := callString(ctx, con, "name"); if err != nil { log.Fatal(err) }
			symbol, err := callString(ctx, con, "symbol"); if err != nil { log.Fatal(err) }
			decimals, err := callUint8(ctx, con, "decimals"); if err != nil { log.Fatal(err) }
			total, err := callBig(ctx, con, "totalSupply"); if err != nil { log.Fatal(err) }
			paused, err := callBool(ctx, con, "paused"); if err != nil { log.Fatal(err) }
			fmt.Printf("name=%s symbol=%s decimals=%d totalSupply=%s paused=%v\n", name, symbol, decimals, total.String(), paused)
		},
	})

	// balance
	var balAddr string
	balCmd := &cobra.Command{
		Use:   "balance",
		Short: "Get ERC20 balance of an address",
		Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if balAddr == "" { log.Fatal("--address required") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			bal, err := callBig(ctx, con, "balanceOf", common.HexToAddress(balAddr)); if err != nil { log.Fatal(err) }
			fmt.Println(bal.String())
		},
	}
	balCmd.Flags().StringVar(&balAddr, "address", "", "Address to query")
	coinCmd.AddCommand(balCmd)

	// transfer
	var trTo, trAmt string
	trCmd := &cobra.Command{
		Use:   "transfer",
		Short: "Transfer ERC20 tokens (base units)",
		Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if trTo == "" || trAmt == "" { log.Fatal("--to and --amount required") }
			amt, err := toWei(trAmt); if err != nil { log.Fatal(err) }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx)
			auth := txOpts(ctx, c, chain)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			tx, err := con.Transact(auth, "transfer", common.HexToAddress(trTo), amt); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		},
	}
	trCmd.Flags().StringVar(&trTo, "to", "", "Recipient address")
	trCmd.Flags().StringVar(&trAmt, "amount", "", "Amount in base units")
	coinCmd.AddCommand(trCmd)

	// approve
	var apSp, apAmt string
	apCmd := &cobra.Command{
		Use:   "approve",
		Short: "Approve spender (base units)",
		Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if apSp == "" || apAmt == "" { log.Fatal("--spender and --amount required") }
			amt, err := toWei(apAmt); if err != nil { log.Fatal(err) }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			tx, err := con.Transact(auth, "approve", common.HexToAddress(apSp), amt); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		},
	}
	apCmd.Flags().StringVar(&apSp, "spender", "", "Spender address")
	apCmd.Flags().StringVar(&apAmt, "amount", "", "Amount in base units")
	coinCmd.AddCommand(apCmd)

	// allowance
	var alOwn, alSp string
	alCmd := &cobra.Command{
		Use:   "allowance",
		Short: "Check allowance(owner,spender)",
		Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if alOwn == "" || alSp == "" { log.Fatal("--owner and --spender required") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			v, err := callBig(ctx, con, "allowance", common.HexToAddress(alOwn), common.HexToAddress(alSp)); if err != nil { log.Fatal(err) }
			fmt.Println(v.String())
		},
	}
	alCmd.Flags().StringVar(&alOwn, "owner", "", "Owner address")
	alCmd.Flags().StringVar(&alSp, "spender", "", "Spender address")
	coinCmd.AddCommand(alCmd)

	// transfer-from
	var tfFrom, tfTo, tfAmt string
	tfCmd := &cobra.Command{
		Use:   "transfer-from",
		Short: "Transfer from (requires allowance)",
		Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if tfFrom == "" || tfTo == "" || tfAmt == "" { log.Fatal("--from --to --amount required") }
			amt, err := toWei(tfAmt); if err != nil { log.Fatal(err) }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			tx, err := con.Transact(auth, "transferFrom", common.HexToAddress(tfFrom), common.HexToAddress(tfTo), amt); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		},
	}
	tfCmd.Flags().StringVar(&tfFrom, "from", "", "From address")
	tfCmd.Flags().StringVar(&tfTo, "to", "", "To address")
	tfCmd.Flags().StringVar(&tfAmt, "amount", "", "Amount in base units")
	coinCmd.AddCommand(tfCmd)

	// paused
	coinCmd.AddCommand(&cobra.Command{Use: "paused", Short: "Is paused?", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
		c, _ := mustClient(ctx)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		p, err := callBool(ctx, con, "paused"); if err != nil { log.Fatal(err) }
		fmt.Println(p)
	}})

	// pause/unpause
	coinCmd.AddCommand(&cobra.Command{Use: "pause", Short: "Pause transfers", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		tx, err := con.Transact(auth, "pause"); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}})
	coinCmd.AddCommand(&cobra.Command{Use: "unpause", Short: "Unpause transfers", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		tx, err := con.Transact(auth, "unpause"); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}})

	// blacklist
	var blAcc string; var blStatus bool
	blCmd := &cobra.Command{Use: "set-blacklist", Short: "Set blacklist status", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); if blAcc=="" { log.Fatal("--account required") }
		ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		tx, err := con.Transact(auth, "setBlacklist", common.HexToAddress(blAcc), blStatus); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}}
	blCmd.Flags().StringVar(&blAcc, "account", "", "Account address")
	blCmd.Flags().BoolVar(&blStatus, "status", false, "Blacklist status")
	coinCmd.AddCommand(blCmd)

	var qblAcc string
	{
		cmd := &cobra.Command{Use: "is-blacklisted", Short: "Check blacklist", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if qblAcc=="" { log.Fatal("--account required") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			v, err := callBool(ctx, con, "isBlacklisted", common.HexToAddress(qblAcc)); if err != nil { log.Fatal(err) }
			fmt.Println(v)
		}}
		cmd.Flags().StringVar(&qblAcc, "account", "", "Account address")
		coinCmd.AddCommand(cmd)
	}

	// mint
	var miTo, miAmt string
	miCmd := &cobra.Command{Use: "mint", Short: "Mint tokens (admin only)", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); if miTo==""||miAmt=="" { log.Fatal("--to --amount required") }
		amt, err := toWei(miAmt); if err!=nil { log.Fatal(err) }
		ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		tx, err := con.Transact(auth, "mint", common.HexToAddress(miTo), amt); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}}
	miCmd.Flags().StringVar(&miTo, "to", "", "Recipient address")
	miCmd.Flags().StringVar(&miAmt, "amount", "", "Amount in base units")
	coinCmd.AddCommand(miCmd)

	// burn & burn-from
	var buAmt string
	{
		cmd := &cobra.Command{Use: "burn", Short: "Burn own balance (admin only)", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if buAmt=="" { log.Fatal("--amount required") }
			amt, err := toWei(buAmt); if err!=nil { log.Fatal(err) }
			ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			tx, err := con.Transact(auth, "burn", amt); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&buAmt, "amount", "", "Amount in base units")
		coinCmd.AddCommand(cmd)
	}

	var bfAcc, bfAmt string
	bfCmd := &cobra.Command{Use: "burn-from", Short: "Burn from account (admin only)", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); if bfAcc==""||bfAmt=="" { log.Fatal("--account --amount required") }
		amt, err := toWei(bfAmt); if err!=nil { log.Fatal(err) }
		ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		tx, err := con.Transact(auth, "burnFrom", common.HexToAddress(bfAcc), amt); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}}
	bfCmd.Flags().StringVar(&bfAcc, "account", "", "Account")
	bfCmd.Flags().StringVar(&bfAmt, "amount", "", "Amount in base units")
	coinCmd.AddCommand(bfCmd)

	// roles
	var rrRole, rrAcc string
	grCmd := &cobra.Command{Use: "grant-role", Short: "Grant role to account", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); if rrRole==""||rrAcc=="" { log.Fatal("--role --account required") }
		ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		role := roleID(rrRole)
		tx, err := con.Transact(auth, "grantRole", role, common.HexToAddress(rrAcc)); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}}
	grCmd.Flags().StringVar(&rrRole, "role", "", "Role name (DEFAULT_ADMIN_ROLE, PAUSER_ROLE, MINTER_ROLE, COMPLIANCE_ROLE)")
	grCmd.Flags().StringVar(&rrAcc, "account", "", "Account address")
	coinCmd.AddCommand(grCmd)

	rrRole, rrAcc = "", ""
	rvCmd := &cobra.Command{Use: "revoke-role", Short: "Revoke role from account", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); if rrRole==""||rrAcc=="" { log.Fatal("--role --account required") }
		ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		role := roleID(rrRole)
		tx, err := con.Transact(auth, "revokeRole", role, common.HexToAddress(rrAcc)); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}}
	rvCmd.Flags().StringVar(&rrRole, "role", "", "Role name")
	rvCmd.Flags().StringVar(&rrAcc, "account", "", "Account address")
	coinCmd.AddCommand(rvCmd)

	rrRole, rrAcc = "", ""
	hasCmd := &cobra.Command{Use: "has-role", Short: "Check role for account", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); if rrRole==""||rrAcc=="" { log.Fatal("--role --account required") }
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
		c, _ := mustClient(ctx)
		addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
		con := boundContract(addr, easyBRLStableABI, c)
		role := roleID(rrRole)
		v, err := callBool(ctx, con, "hasRole", role, common.HexToAddress(rrAcc)); if err != nil { log.Fatal(err) }
		fmt.Println(v)
	}} 
	hasCmd.Flags().StringVar(&rrRole, "role", "", "Role name")
	hasCmd.Flags().StringVar(&rrAcc, "account", "", "Account address")
	coinCmd.AddCommand(hasCmd)

	var gcAcc string
	{
		cmd := &cobra.Command{Use: "grant-compliance-role", Short: "Grant COMPLIANCE_ROLE via helper", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if gcAcc=="" { log.Fatal("--account required") }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(coinAddr, "EASY_COIN_ADDR")
			con := boundContract(addr, easyBRLStableABI, c)
			tx, err := con.Transact(auth, "grantComplianceRole", common.HexToAddress(gcAcc)); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&gcAcc, "account", "", "Account address")
		coinCmd.AddCommand(cmd)
	}

	return coinCmd
}

func cmdToken() *cobra.Command {
    tokCmd := &cobra.Command{Use: "token", Short: "Interact with EasyToken (ERC721)"}

	// info
	tokCmd.AddCommand(&cobra.Command{Use: "info", Short: "Show ERC721 name, symbol, paused", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
		c, _ := mustClient(ctx)
		addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
		con := boundContract(addr, easyTokenDocumentABI, c)
		name, err := callString(ctx, con, "name"); if err != nil { log.Fatal(err) }
		symbol, err := callString(ctx, con, "symbol"); if err != nil { log.Fatal(err) }
		paused, err := callBool(ctx, con, "paused"); if err != nil { log.Fatal(err) }
		fmt.Printf("name=%s symbol=%s paused=%v\n", name, symbol, paused)
	}})

	// owner-of
	var ooID string
	{
		cmd := &cobra.Command{Use: "owner-of", Short: "Owner of tokenId", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if ooID=="" { log.Fatal("--token-id required") }
			id, ok := new(big.Int).SetString(ooID, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			owner, err := callAddress(ctx, con, "ownerOf", id); if err != nil { log.Fatal(err) }
			fmt.Println(owner.Hex())
		}}
		cmd.Flags().StringVar(&ooID, "token-id", "", "Token ID")
		tokCmd.AddCommand(cmd)
	}

	// balance
	var tbAddr string
	{
		cmd := &cobra.Command{Use: "balance", Short: "Number of NFTs for address", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if tbAddr=="" { log.Fatal("--address required") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			bal, err := callBig(ctx, con, "balanceOf", common.HexToAddress(tbAddr)); if err != nil { log.Fatal(err) }
			fmt.Println(bal.String())
		}}
		cmd.Flags().StringVar(&tbAddr, "address", "", "Address")
		tokCmd.AddCommand(cmd)
	}

	// token-uri
	var tuID string
	{
		cmd := &cobra.Command{Use: "token-uri", Short: "Get tokenURI", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if tuID=="" { log.Fatal("--token-id required") }
			id, ok := new(big.Int).SetString(tuID, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			uri, err := callString(ctx, con, "tokenURI", id); if err != nil { log.Fatal(err) }
			fmt.Println(uri)
		}}
		cmd.Flags().StringVar(&tuID, "token-id", "", "Token ID")
		tokCmd.AddCommand(cmd)
	}

	// doc-hash
	var dhID string
	{
		cmd := &cobra.Command{Use: "doc-hash", Short: "Get document hash", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if dhID=="" { log.Fatal("--token-id required") }
			id, ok := new(big.Int).SetString(dhID, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			h, err := callBytes32(ctx, con, "documentHashOf", id); if err != nil { log.Fatal(err) }
			fmt.Println("0x" + common.Bytes2Hex(h[:]))
		}}
		cmd.Flags().StringVar(&dhID, "token-id", "", "Token ID")
		tokCmd.AddCommand(cmd)
	}

	// appraisal
	var apID string
	{
		cmd := &cobra.Command{Use: "appraisal", Short: "Get appraisal value (base units)", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if apID=="" { log.Fatal("--token-id required") }
			id, ok := new(big.Int).SetString(apID, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			v, err := callBig(ctx, con, "appraisalOf", id); if err != nil { log.Fatal(err) }
			fmt.Println(v.String())
		}}
		cmd.Flags().StringVar(&apID, "token-id", "", "Token ID")
		tokCmd.AddCommand(cmd)
	}

	// set-token-uri
	var stuID, stuURI string
	{
		cmd := &cobra.Command{Use: "set-token-uri", Short: "Set token URI (METADATA_ROLE)", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if stuID==""||stuURI=="" { log.Fatal("--token-id --uri required") }
			id, ok := new(big.Int).SetString(stuID, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "setTokenURI", id, stuURI); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&stuID, "token-id", "", "Token ID")
		cmd.Flags().StringVar(&stuURI, "uri", "", "New URI")
		tokCmd.AddCommand(cmd)
	}

	// set-appraisal
	var sapID, sapVal string
	{
		cmd := &cobra.Command{Use: "set-appraisal", Short: "Set appraisal value (base units)", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if sapID==""||sapVal=="" { log.Fatal("--token-id --value required") }
			id, ok := new(big.Int).SetString(sapID, 10); if !ok { log.Fatal("invalid token-id") }
			val, err := toWei(sapVal); if err != nil { log.Fatal(err) }
			ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "setAppraisal", id, val); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&sapID, "token-id", "", "Token ID")
		cmd.Flags().StringVar(&sapVal, "value", "", "Appraisal in base units")
		tokCmd.AddCommand(cmd)
	}

	// safe-mint
	var smTo, smURI, smHash, smVal string
	{
		cmd := &cobra.Command{Use: "safe-mint", Short: "Mint new document NFT", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if smTo==""||smURI==""||smHash==""||smVal=="" { log.Fatal("--to --uri --hash --value required") }
			h, err := fromHex32(smHash); if err != nil { log.Fatal(err) }
			v, err := toWei(smVal); if err != nil { log.Fatal(err) }
			ctx, cancel := context.WithTimeout(context.Background(), 240*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "safeMint", common.HexToAddress(smTo), smURI, h, v); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&smTo, "to", "", "Recipient address")
		cmd.Flags().StringVar(&smURI, "uri", "", "Document URL")
		cmd.Flags().StringVar(&smHash, "hash", "", "Document hash (0x + 64 hex)")
		cmd.Flags().StringVar(&smVal, "value", "", "Appraisal in base units")
		tokCmd.AddCommand(cmd)
	}

	// transfer & safe-transfer
	var ttTo, ttID string
	{
		cmd := &cobra.Command{Use: "transfer", Short: "transferFrom(sender,to,tokenId)", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if ttTo==""||ttID=="" { log.Fatal("--to --token-id required") }
			id, ok := new(big.Int).SetString(ttID, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			sender := crypto.PubkeyToAddress(mustPrivKey().PublicKey)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "safeTransferFrom", sender, common.HexToAddress(ttTo), id); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&ttTo, "to", "", "Recipient address")
		cmd.Flags().StringVar(&ttID, "token-id", "", "Token ID")
		tokCmd.AddCommand(cmd)
	}

	var stTo, stID string
	{
		cmd := &cobra.Command{Use: "safe-transfer", Short: "safeTransferFrom(sender,to,tokenId)", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if stTo==""||stID=="" { log.Fatal("--to --token-id required") }
			id, ok := new(big.Int).SetString(stID, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			sender := crypto.PubkeyToAddress(mustPrivKey().PublicKey)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "safeTransferFrom", sender, common.HexToAddress(stTo), id); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&stTo, "to", "", "Recipient address")
		cmd.Flags().StringVar(&stID, "token-id", "", "Token ID")
		tokCmd.AddCommand(cmd)
	}

	// approvals
	var apTo string; var apID2 string
	{
		cmd := &cobra.Command{Use: "approve", Short: "Approve address for tokenId", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if apTo==""||apID2=="" { log.Fatal("--to --token-id required") }
			id, ok := new(big.Int).SetString(apID2, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "approve", common.HexToAddress(apTo), id); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&apTo, "to", "", "Approved address")
		cmd.Flags().StringVar(&apID2, "token-id", "", "Token ID")
		tokCmd.AddCommand(cmd)
	}

	var saoOp string; var saoApproved bool
	{
		cmd := &cobra.Command{Use: "set-approval-for-all", Short: "Set operator approval", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if saoOp=="" { log.Fatal("--operator required") }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "setApprovalForAll", common.HexToAddress(saoOp), saoApproved); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&saoOp, "operator", "", "Operator address")
		cmd.Flags().BoolVar(&saoApproved, "approved", false, "Approved status")
		tokCmd.AddCommand(cmd)
	}

	// getters for approvals
	var gaID string
	{
		cmd := &cobra.Command{Use: "get-approved", Short: "Get approved for tokenId", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if gaID=="" { log.Fatal("--token-id required") }
			id, ok := new(big.Int).SetString(gaID, 10); if !ok { log.Fatal("invalid token-id") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			a, err := callAddress(ctx, con, "getApproved", id); if err != nil { log.Fatal(err) }
			fmt.Println(a.Hex())
		}}
		cmd.Flags().StringVar(&gaID, "token-id", "", "Token ID")
		tokCmd.AddCommand(cmd)
	}

	var iaOwner, iaOp string
	{
		cmd := &cobra.Command{Use: "is-approved-for-all", Short: "Is operator approved for owner", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if iaOwner==""||iaOp=="" { log.Fatal("--owner --operator required") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			v, err := callBool(ctx, con, "isApprovedForAll", common.HexToAddress(iaOwner), common.HexToAddress(iaOp)); if err != nil { log.Fatal(err) }
			fmt.Println(v)
		}}
		cmd.Flags().StringVar(&iaOwner, "owner", "", "Owner address")
		cmd.Flags().StringVar(&iaOp, "operator", "", "Operator address")
		tokCmd.AddCommand(cmd)
	}

	// pause/unpause/paused
	tokCmd.AddCommand(&cobra.Command{Use: "paused", Short: "Is paused?", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
		c, _ := mustClient(ctx)
		addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
		con := boundContract(addr, easyTokenDocumentABI, c)
		p, err := callBool(ctx, con, "paused"); if err != nil { log.Fatal(err) }
		fmt.Println(p)
	}})

	tokCmd.AddCommand(&cobra.Command{Use: "pause", Short: "Pause transfers", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
		con := boundContract(addr, easyTokenDocumentABI, c)
		tx, err := con.Transact(auth, "pause"); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}})

	tokCmd.AddCommand(&cobra.Command{Use: "unpause", Short: "Unpause transfers", Run: func(cmd *cobra.Command, args []string) {
		loadEnv(); ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
		c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
		addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
		con := boundContract(addr, easyTokenDocumentABI, c)
		tx, err := con.Transact(auth, "unpause"); if err != nil { log.Fatal(err) }
		fmt.Println(tx.Hash().Hex())
	}})

	// blacklist
	var tblAcc string; var tblStatus bool
	{
		cmd := &cobra.Command{Use: "set-blacklist", Short: "Set blacklist status", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if tblAcc=="" { log.Fatal("--account required") }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "setBlacklist", common.HexToAddress(tblAcc), tblStatus); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&tblAcc, "account", "", "Account")
		cmd.Flags().BoolVar(&tblStatus, "status", false, "Blacklist status")
		tokCmd.AddCommand(cmd)
	}

	var tqblAcc string
	{
		cmd := &cobra.Command{Use: "is-blacklisted", Short: "Check blacklist", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if tqblAcc=="" { log.Fatal("--account required") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			v, err := callBool(ctx, con, "isBlacklisted", common.HexToAddress(tqblAcc)); if err != nil { log.Fatal(err) }
			fmt.Println(v)
		}}
		cmd.Flags().StringVar(&tqblAcc, "account", "", "Account")
		tokCmd.AddCommand(cmd)
	}

	// roles
	var trRole, trAcc string
	{
		cmd := &cobra.Command{Use: "grant-role", Short: "Grant role", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if trRole==""||trAcc=="" { log.Fatal("--role --account required") }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			role := roleID(trRole)
			tx, err := con.Transact(auth, "grantRole", role, common.HexToAddress(trAcc)); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&trRole, "role", "", "Role name (DEFAULT_ADMIN_ROLE, PAUSER_ROLE, MINTER_ROLE, METADATA_ROLE, APPRAISER_ROLE, COMPLIANCE_ROLE)")
		cmd.Flags().StringVar(&trAcc, "account", "", "Account address")
		tokCmd.AddCommand(cmd)
	}

	trRole, trAcc = "", ""
	{
		cmd := &cobra.Command{Use: "revoke-role", Short: "Revoke role", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if trRole==""||trAcc=="" { log.Fatal("--role --account required") }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			role := roleID(trRole)
			tx, err := con.Transact(auth, "revokeRole", role, common.HexToAddress(trAcc)); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&trRole, "role", "", "Role name")
		cmd.Flags().StringVar(&trAcc, "account", "", "Account address")
		tokCmd.AddCommand(cmd)
	}

	trRole, trAcc = "", ""
	{
		cmd := &cobra.Command{Use: "has-role", Short: "Has role?", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if trRole==""||trAcc=="" { log.Fatal("--role --account required") }
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()
			c, _ := mustClient(ctx)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			role := roleID(trRole)
			v, err := callBool(ctx, con, "hasRole", role, common.HexToAddress(trAcc)); if err != nil { log.Fatal(err) }
			fmt.Println(v)
		}}
		cmd.Flags().StringVar(&trRole, "role", "", "Role name")
		cmd.Flags().StringVar(&trAcc, "account", "", "Account address")
		tokCmd.AddCommand(cmd)
	}

	var tgcAcc string
	{
		cmd := &cobra.Command{Use: "grant-compliance-role", Short: "Grant COMPLIANCE_ROLE via helper", Run: func(cmd *cobra.Command, args []string) {
			loadEnv(); if tgcAcc=="" { log.Fatal("--account required") }
			ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second); defer cancel()
			c, chain := mustClient(ctx); auth := txOpts(ctx, c, chain)
			addr := mustAddress(tokenAddr, "EASY_TOKEN_ADDR")
			con := boundContract(addr, easyTokenDocumentABI, c)
			tx, err := con.Transact(auth, "grantComplianceRole", common.HexToAddress(tgcAcc)); if err != nil { log.Fatal(err) }
			fmt.Println(tx.Hash().Hex())
		}}
		cmd.Flags().StringVar(&tgcAcc, "account", "", "Account address")
		tokCmd.AddCommand(cmd)
	}

	return tokCmd
}

func cmdPolygon() *cobra.Command {
	pg := &cobra.Command{Use: "polygon", Short: "Polygon utilities"}
	pg.AddCommand(&cobra.Command{Use: "create-new-address", Short: "Generate a new wallet", Run: func(cmd *cobra.Command, args []string) {
		pk, err := crypto.GenerateKey(); if err != nil { log.Fatal(err) }
		pkHex := "0x" + common.Bytes2Hex(crypto.FromECDSA(pk))
		pubHex := "0x" + common.Bytes2Hex(crypto.FromECDSAPub(&pk.PublicKey))
		addr := crypto.PubkeyToAddress(pk.PublicKey).Hex()
		fmt.Printf("privateKey=%s\npublicKey=%s\naddress=%s\n", pkHex, pubHex, addr)
	}})
	return pg
}

func main() {
	root := &cobra.Command{Use: "sdk", Short: "CLI for EasyBRL (coin) and EasyToken (token)"}
	root.PersistentFlags().StringVar(&rpcURL, "rpc", "", "RPC URL (overrides .env)")
	root.PersistentFlags().StringVar(&pkHex, "pk", "", "Private key hex (overrides .env)")
	root.PersistentFlags().StringVar(&coinAddr, "coin-addr", coinAddr, "ERC20 contract address (overrides .env)")
	root.PersistentFlags().StringVar(&tokenAddr, "token-addr", tokenAddr, "ERC721 contract address (overrides .env)")

	root.AddCommand(cmdCoin())
	root.AddCommand(cmdToken())
	root.AddCommand(cmdPolygon())

	if err := root.Execute(); err != nil {
		os.Exit(1)
	}
}
