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(""), 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) } { cmd := &cobra.Command{Use: "get-info [token-id]", Short: "Show owner and content", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { loadEnv() id, ok := new(big.Int).SetString(args[0], 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) } content, err := callString(ctx, con, "contentOf", id); if err != nil { log.Fatal(err) } fmt.Printf("owner=%s\n", owner.Hex()) fmt.Printf("content=%s\n", content) }} 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) } // mint content { cmd := &cobra.Command{Use: "mint [content]", Short: "Mint new token content (<=20 chars)", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { content := args[0] if content == "" { log.Fatal("content required") } if len([]byte(content)) > 20 { log.Fatal("content must be at most 20 bytes") } loadEnv() 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) sender := crypto.PubkeyToAddress(mustPrivKey().PublicKey) tx, err := con.Transact(auth, "mintContent", sender, content); if err != nil { log.Fatal(err) } receipt, err := bind.WaitMined(ctx, c, tx); if err != nil { log.Fatal(err) } mintSig := crypto.Keccak256Hash([]byte("ContentMinted(uint256,address,string)")) tokenID := new(big.Int) for _, lg := range receipt.Logs { if lg.Address == addr && len(lg.Topics) >= 3 && lg.Topics[0] == mintSig { tokenID.SetBytes(lg.Topics[1].Bytes()) break } } fmt.Printf("tx=%s\n", tx.Hash().Hex()) if tokenID.Sign() > 0 { fmt.Printf("tokenId=%s\n", tokenID.String()) } }} 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) } }