Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions accounts/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package accounts

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"os"

account "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/unpackdev/solgo/clients"
)

// Account represents an Ethereum account with extended functionalities.
// It embeds ClientPool for network interactions and KeyStore for account management.
// It also includes fields for account details, network information, and additional tags.
type Account struct {
*clients.ClientPool `json:"-" yaml:"-"` // ClientPool for Ethereum client interactions
*keystore.KeyStore `json:"-" yaml:"-"` // KeyStore for managing account keys
KeystoreAccount account.Account `json:"account" yaml:"account"` // Ethereum account information
Password string `json:"password" yaml:"password"` // Account's password
Network Network `json:"network" yaml:"network"` // Network information
Tags []string `json:"tags" yaml:"tags"` // Arbitrary tags for the account
}

// HasTag checks if the account has a specific tag.
// Returns true if the tag exists, false otherwise.
func (a *Account) HasTag(tag string) bool {
for _, t := range a.Tags {
if t == tag {
return true
}
}

return false
}

// DecodePassword decodes the base64-encoded password.
// Returns the decoded password or an error if decoding fails.
func (a *Account) DecodePassword() (string, error) {
passwd, err := base64.StdEncoding.DecodeString(a.Password)
return string(passwd), err
}

// GetAddress retrieves the Ethereum address of the account.
func (a *Account) GetAddress() common.Address {
return a.KeystoreAccount.Address
}

// Balance queries the Ethereum network for the account's balance at a specific block number.
// Returns the balance or an error if the query fails.
func (a *Account) Balance(ctx context.Context, blockNum *big.Int) (*big.Int, error) {
client := a.ClientPool.GetClientByGroup(string(a.Network))
if client == nil {
return big.NewInt(0), fmt.Errorf("no client found for network %s", string(a.Network))
}

balance, err := client.BalanceAt(ctx, a.KeystoreAccount.Address, blockNum)
if err != nil {
return big.NewInt(0), err
}

return balance, nil
}

// Transfer handles the transfer of Ethereum from the account to another address.
// Validates sufficient balance and signs the transaction with the account's passphrase.
// Returns the signed transaction or an error if the transfer fails.
func (a *Account) Transfer(ctx context.Context, to common.Address, value *big.Int) (*types.Transaction, error) {
client := a.ClientPool.GetClientByGroup(string(a.Network))
if client == nil {
return nil, fmt.Errorf("no client found for network %s", string(a.Network))
}

currentBalance, err := a.Balance(ctx, nil)
if err != nil {
return nil, err
}

if currentBalance.Cmp(value) < 0 {
return nil, fmt.Errorf("insufficient balance")
}

passwd, err := a.DecodePassword()
if err != nil {
return nil, fmt.Errorf("failed to decode password: %s", err.Error())
}

nonce, err := client.PendingNonceAt(context.Background(), a.KeystoreAccount.Address)
if err != nil {
return nil, err
}

gasLimit := uint64(21000)
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
return nil, err
}

var data []byte
tx := types.NewTransaction(nonce, to, value, gasLimit, gasPrice, data)

signedTx, err := a.KeyStore.SignTxWithPassphrase(a.KeystoreAccount, passwd, tx, big.NewInt(client.GetNetworkID()))
if err != nil {
return nil, err
}

if err := client.SendTransaction(ctx, signedTx); err != nil {
return nil, err
}

return signedTx, nil
}

// SaveToPath saves the account information to a specified file path in JSON format.
// Returns an error if the saving process fails.
func (a *Account) SaveToPath(path string) error {
file, err := json.MarshalIndent(a, "", " ")
if err != nil {
return err
}

return os.WriteFile(path, file, 0644)
}

// LoadAccount is a utility function to load an account from a JSON file at a given path.
// Returns the loaded Account or an error if loading fails.
func LoadAccount(path string) (*Account, error) {
file, err := os.ReadFile(path)
if err != nil {
return nil, err
}

var account Account
err = json.Unmarshal(file, &account)
if err != nil {
return nil, err
}

return &account, nil
}
8 changes: 8 additions & 0 deletions accounts/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Package accounts provides a comprehensive suite of tools and types for managing Ethereum and Ethereum-compatible
// blockchain accounts. It facilitates various operations such as account creation, importing, listing, and retrieval,
// alongside managing their corresponding keystore files for different networks.

// The package is designed to be flexible and adaptable to various Ethereum-compatible networks like Ethereum mainnet,
// Binance Smart Chain (BSC), and Polygon. It provides a structured approach to handle accounts across these networks,
// making it easier for developers to interact with different blockchain environments through a unified interface.
package accounts
191 changes: 191 additions & 0 deletions accounts/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package accounts

import (
"context"
"encoding/base64"
"fmt"
"os"
"path"
"strings"

"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/unpackdev/solgo/clients"
"github.com/unpackdev/solgo/utils"
)

// Manager handles the account operations across various Ethereum networks.
// It maintains a map of keystores and accounts for each supported network.
type Manager struct {
ctx context.Context // Context for managing async operations
cfg *Options // Configuration options for the Manager
client *clients.ClientPool // Ethereum client pool
ks map[Network]*keystore.KeyStore // Keystores for different networks
accounts map[Network][]*Account // Accounts mapped by their network
}

// NewManager initializes a new Manager instance.
// It checks for the existence of keystore paths and supported networks,
// and loads existing accounts from the keystore.
func NewManager(ctx context.Context, client *clients.ClientPool, cfg *Options) (*Manager, error) {
if !utils.PathExists(cfg.KeystorePath) {
return nil, fmt.Errorf("keystore path does not exist: %s", cfg.KeystorePath)
}

if len(cfg.SupportedNetworks) == 0 {
return nil, fmt.Errorf("no supported networks provided. You must provide at least one network")
}

var keystores = make(map[Network]*keystore.KeyStore)

// Now for each supported network, we need to create a subdirectory in the keystore path if it does not exist.
// Be sure that write permissions are set correctly.
for _, network := range cfg.SupportedNetworks {
networkPath := path.Join(cfg.KeystorePath, strings.ToLower(string(network)))
if !utils.PathExists(networkPath) {
if err := os.MkdirAll(networkPath, 0700); err != nil {
return nil, err
}
}

keystores[network] = keystore.NewKeyStore(
networkPath,
keystore.StandardScryptN,
keystore.StandardScryptP,
)
}

toReturn := &Manager{
ctx: ctx,
cfg: cfg,
ks: keystores,
client: client,
accounts: make(map[Network][]*Account),
}

if err := toReturn.Load(); err != nil {
return nil, err
}

return toReturn, nil
}

// Load loads accounts from the keystore for each network.
// Returns an error if it fails to load accounts for any network.
func (m *Manager) Load() error {
for _, network := range m.cfg.SupportedNetworks {
ks, err := m.GetKeystore(network)
if err != nil {
return err
}

for _, kAcc := range ks.Accounts() {
path := path.Join(m.GetNetworkPath(network), kAcc.Address.Hex()+".json")
acc, err := LoadAccount(path)
if err != nil {
return err
}
acc.ClientPool = m.client
acc.KeyStore = ks
m.accounts[network] = append(m.accounts[network], acc)
}
}

return nil
}

// GetConfig returns the configuration options of the Manager.
func (m *Manager) GetConfig() *Options {
return m.cfg
}

// GetNetworkPath returns the file path for a given network's keystore.
func (m *Manager) GetNetworkPath(network Network) string {
return path.Join(m.cfg.KeystorePath, strings.ToLower(string(network)))
}

// GetKeystore retrieves the keystore for a given network.
// Returns an error if the network is not supported.
func (m *Manager) GetKeystore(network Network) (*keystore.KeyStore, error) {
if _, ok := m.ks[network]; !ok {
return nil, fmt.Errorf("network %s is not supported", network)
}

return m.ks[network], nil
}

// Import imports an account with a given private key and password into the keystore of a specified network.
// TODO: Currently, the function body is empty and needs implementation.
func (m *Manager) Import(network Network, privateKey string, password string) error {
return nil
}

// Create creates a new account for a given network with a specified password and optional tags.
// It saves the account to the network's keystore path and adds it to the accounts map.
func (m *Manager) Create(network Network, password string, tags ...string) (*Account, error) {
ks, err := m.GetKeystore(network)
if err != nil {
return nil, err
}

kacc, err := ks.NewAccount(password)
if err != nil {
return nil, err
}

acc := &Account{
ClientPool: m.client,
KeyStore: ks,
KeystoreAccount: kacc,
Password: base64.StdEncoding.EncodeToString([]byte(password)),
Network: network,
Tags: tags,
}

// Now we need to save the account to the keystore path.
path := path.Join(m.GetNetworkPath(network), kacc.Address.Hex()+".json")

if err := acc.SaveToPath(path); err != nil {
return nil, err
}

// Now we need to add the account to the accounts map.
m.accounts[network] = append(m.accounts[network], acc)

return acc, nil
}

// List lists all accounts for a given network, optionally filtered by tags.
func (m *Manager) List(network Network, tags ...string) []*Account {
var toReturn []*Account

if accounts, ok := m.accounts[network]; ok {
if len(tags) == 0 {
return accounts
}

for _, acc := range accounts {
for _, tag := range tags {
if acc.HasTag(tag) {
toReturn = append(toReturn, acc)
}
}
}
}

return toReturn
}

// Get retrieves a specific account by its address for a given network.
// Returns an error if the account is not found.
func (m *Manager) Get(network Network, address common.Address) (*Account, error) {
if accounts, ok := m.accounts[network]; ok {
for _, acc := range accounts {
if acc.KeystoreAccount.Address.Hex() == address.Hex() {
return acc, nil
}
}
}

return nil, fmt.Errorf("account not found")
}
12 changes: 12 additions & 0 deletions accounts/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package accounts

// Options defines the configuration parameters for account management.
type Options struct {
// KeystorePath specifies the file system path to the directory where the keystore files are stored.
// The keystore is used to securely store the private keys of Ethereum accounts.
KeystorePath string `json:"keystore_path" yaml:"keystore_path"`

// SupportedNetworks lists the Ethereum based networks that the account manager will interact with.
// Each network has a corresponding keystore and set of account configurations.
SupportedNetworks []Network `json:"supported_networks" yaml:"supported_networks"`
}
16 changes: 16 additions & 0 deletions accounts/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package accounts

// Network defines a type for representing various Ethereum-compatible networks.
// It is used to specify and differentiate between different blockchain networks.
type Network string

const (
// Ethereum represents the Ethereum mainnet.
Ethereum Network = "ethereum"

// Bsc represents the Binance Smart Chain network.
Bsc Network = "bsc"

// Polygon represents the Polygon (formerly Matic Network) network.
Polygon Network = "polygon"
)
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
github.com/ethereum/c-kzg-4844 v0.3.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
Expand Down
Loading