diff --git a/block.go b/block.go index d34e7ab..99de354 100644 --- a/block.go +++ b/block.go @@ -12,6 +12,7 @@ var ( maxNonce = math.MaxInt64 ) +// Block repressents a block in the blockchain type Block struct { Timestamp int64 Transactions []*Transaction diff --git a/blockchain.go b/blockchain.go index b630d9e..4d8f8c3 100644 --- a/blockchain.go +++ b/blockchain.go @@ -1,7 +1,10 @@ package main import ( + "bytes" + "crypto/ecdsa" "encoding/hex" + "errors" "fmt" "os" @@ -108,7 +111,7 @@ func NewGenesisBlock(coinbase *Transaction) *Block { } // FindUnspentTransactions returns a list of transactions containing unspent outputs -func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction { +func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction { var unspentTXs []Transaction spentTXOs := make(map[string][]int) bci := bc.Iterator() @@ -129,14 +132,14 @@ func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction { } } - if out.CanBeUnlockedWith(address) { + if out.IsLockedWithKey(pubKeyHash) { unspentTXs = append(unspentTXs, *tx) } } if tx.IsCoinbase() == false { for _, in := range tx.Vin { - if in.CanUnlockOutputWith(address) { + if in.UsesKey(pubKeyHash) { inTxID := hex.EncodeToString(in.Txid) spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout) } @@ -153,12 +156,12 @@ func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction { } // FindUTXO finds and returns all unspent transaction outputs -func (bc *Blockchain) FindUTXO(address string) []TXOutput { +func (bc *Blockchain) FindUTXO(pubKeyHash []byte) []TXOutput { var UTXOs []TXOutput - unspentTransactions := bc.FindUnspentTransactions(address) + unspentTransactions := bc.FindUnspentTransactions(pubKeyHash) for _, tx := range unspentTransactions { for _, out := range tx.Vout { - if out.CanBeUnlockedWith(address) { + if out.IsLockedWithKey(pubKeyHash) { UTXOs = append(UTXOs, out) } } @@ -167,16 +170,16 @@ func (bc *Blockchain) FindUTXO(address string) []TXOutput { } // FindSpendableOutputs finds and returns unspent outputs to reference in inputs -func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) { +func (bc *Blockchain) FindSpendableOutputs(pubKeyHash []byte, amount int) (int, map[string][]int) { unspentOutputs := make(map[string][]int) - unspentTXs := bc.FindUnspentTransactions(address) + unspentTXs := bc.FindUnspentTransactions(pubKeyHash) accumulated := 0 Work: for _, tx := range unspentTXs { txID := hex.EncodeToString(tx.ID) for outIdx, out := range tx.Vout { - if out.CanBeUnlockedWith(address) && accumulated < amount { + if out.IsLockedWithKey(pubKeyHash) && accumulated < amount { accumulated += out.Value unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) if accumulated >= amount { @@ -192,6 +195,12 @@ Work: func (bc *Blockchain) MineBlock(transactions []*Transaction) { var lastHash []byte + for _, tx := range transactions { + if !bc.VerifyTransaction(tx) { + exitWithError(errors.New("Invalid Transaction")) + } + } + err := bc.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) lastHash = b.Get([]byte("1")) @@ -223,6 +232,49 @@ func (bc *Blockchain) MineBlock(transactions []*Transaction) { } } +func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) { + bci := bc.Iterator() + for { + block := bci.Next() + for _, tx := range block.Transactions { + if bytes.Compare(tx.ID, ID) == 0 { + return *tx, nil + } + } + if len(block.PrevBlockHash) == 0 { + break + } + } + return Transaction{}, errors.New("Transaction not found") +} + +func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) { + prevTXs := make(map[string]Transaction) + + for _, vin := range tx.Vin { + prevTX, err := bc.FindTransaction(vin.Txid) + if err != nil { + exitWithError(err) + } + prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX + } + tx.Sign(privKey, prevTXs) +} + +func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool { + prevTXs := make(map[string]Transaction) + + for _, vin := range tx.Vin { + prevTX, err := bc.FindTransaction(vin.Txid) + if err != nil { + exitWithError(err) + } + prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX + } + + return tx.Verify(prevTXs) +} + // Iterating the Blockchain type BlockchainIterator struct { currentHash []byte diff --git a/cli.go b/cli.go index 4ae6ab3..9f66e32 100644 --- a/cli.go +++ b/cli.go @@ -1,6 +1,7 @@ package main import ( + "errors" "flag" "fmt" "os" @@ -8,10 +9,12 @@ import ( ) const ( - flagBalance = "balance" - flagCreate = "create" - flagSend = "send" - flagLedger = "ledger" + flagBalance = "balance" + flagCreate = "create" + flagSend = "send" + flagLedger = "ledger" + flagCreateWallet = "createwallet" + flagListAddresses = "listaddresses" ) type CLI struct{} @@ -23,6 +26,8 @@ func (cli *CLI) Run() { // Valid CLI flags getBalanceCmd := flag.NewFlagSet(flagBalance, flag.ExitOnError) createBlockchainCmd := flag.NewFlagSet(flagCreate, flag.ExitOnError) + createWalletCmd := flag.NewFlagSet(flagCreateWallet, flag.ExitOnError) + listAddressesCmd := flag.NewFlagSet(flagListAddresses, flag.ExitOnError) sendCmd := flag.NewFlagSet(flagSend, flag.ExitOnError) printChainCmd := flag.NewFlagSet(flagLedger, flag.ExitOnError) @@ -49,6 +54,14 @@ func (cli *CLI) Run() { if err := sendCmd.Parse(os.Args[2:]); err != nil { exitWithError(err) } + case flagCreateWallet: + if err := createWalletCmd.Parse(os.Args[2:]); err != nil { + exitWithError(err) + } + case flagListAddresses: + if err := listAddressesCmd.Parse(os.Args[2:]); err != nil { + exitWithError(err) + } default: cli.printUsage() os.Exit(1) @@ -81,15 +94,67 @@ func (cli *CLI) Run() { } cli.send(*sendFrom, *sendTo, *sendAmount) } + + if createWalletCmd.Parsed() { + cli.createWallet() + } + + if listAddressesCmd.Parsed() { + cli.listAddresses() + } } // createBlockchain creates a new blockchain with a genesis block func (cli *CLI) createBlockchain(address string) { + if !ValidateAddress(address) { + exitWithError(errors.New("Address is not valid")) + } bc := CreateBlockchain(address) bc.db.Close() fmt.Println("Done!") } +// createWallet creates a wallet and outputs the new address +func (cli *CLI) createWallet() { + wallets, _ := NewWallets() + address := wallets.CreateWallet() + wallets.SaveToFile() + + fmt.Printf("Your new address: %s\n", address) +} + +// getBalance retrieves the transaction balance for an address +func (cli *CLI) getBalance(address string) { + if !ValidateAddress(address) { + exitWithError(errors.New("Invalid Address")) + } + bc := NewBlockchain() + defer bc.db.Close() + + balance := 0 + pubKeyHash := Base58Decode([]byte(address)) + pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] + UTXOs := bc.FindUTXO(pubKeyHash) + + for _, out := range UTXOs { + balance += out.Value + } + + fmt.Printf("Balance of '%s': %d\n", address, balance) +} + +// listAddresses outputs all addresses from the wallets file +func (cli *CLI) listAddresses() { + wallets, err := NewWallets() + if err != nil { + exitWithError(err) + } + addresses := wallets.GetAddresses() + for _, address := range addresses { + fmt.Println(address) + } +} + // printChain outputs the entire ledger func (cli *CLI) printChain() { bc := NewBlockchain() @@ -111,21 +176,6 @@ func (cli *CLI) printChain() { } } -// getBalance retrieves the transaction balance for an address -func (cli *CLI) getBalance(address string) { - bc := NewBlockchain() - defer bc.db.Close() - - balance := 0 - UTXOs := bc.FindUTXO(address) - - for _, out := range UTXOs { - balance += out.Value - } - - fmt.Printf("Balance of '%s': %d\n", address, balance) -} - // send Triggers a transaction exchange func (cli *CLI) send(from, to string, amount int) { bc := NewBlockchain() @@ -143,6 +193,8 @@ func (cli *CLI) printUsage() { fmt.Println("\t" + flagCreate + " -address ADDRESS\tCreate a blockchain and send genesis block reward to ADDRESS") fmt.Println("\t" + flagLedger + "\t\t\tprint all the blocks of the blockchain") fmt.Println("\t" + flagSend + " -from FROM -to TO -amount AMOUNT\tSend AMOUNT of coins from FROM address to TO address") + fmt.Println("\t" + flagCreateWallet + "\t\t\tGenerates a new key-pair and saves it into the wallet store") + fmt.Println("\t" + flagListAddresses + "\t\t\tList all addresses from the wallet store") } // validateArgs just makes sure that we've got a couple arguments diff --git a/transaction.go b/transaction.go index 989ee7e..4c0ff8e 100644 --- a/transaction.go +++ b/transaction.go @@ -2,11 +2,16 @@ package main import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "crypto/sha256" "encoding/gob" "encoding/hex" + "errors" "fmt" - "os" + "math/big" + "strings" ) const subsidy = 10 @@ -23,6 +28,131 @@ func (tx Transaction) IsCoinbase() bool { return len(tx.Vin) == 1 && len(tx.Vin[0].Txid) == 0 && tx.Vin[0].Vout == -1 } +// Serialize returns a serialized transaction +func (tx *Transaction) Serialize() []byte { + var encoded bytes.Buffer + + enc := gob.NewEncoder(&encoded) + err := enc.Encode(tx) + if err != nil { + exitWithError(err) + } + + return encoded.Bytes() +} + +// Hash returns the hash of the transaction +func (tx *Transaction) Hash() []byte { + var hash [32]byte + + txCopy := *tx + txCopy.ID = []byte{} + + hash = sha256.Sum256(txCopy.Serialize()) + + return hash[:] +} + +// Sign signs each input of a transaction +func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) { + if tx.IsCoinbase() { + return + } + + for _, vin := range tx.Vin { + if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil { + exitWithError(errors.New("Previous transaction is not correct")) + } + } + + txCopy := tx.TrimmedCopy() + + for inID, vin := range txCopy.Vin { + prevTx := prevTXs[hex.EncodeToString(vin.Txid)] + txCopy.Vin[inID].Signature = nil + txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash + txCopy.ID = txCopy.Hash() + txCopy.Vin[inID].PubKey = nil + + r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID) + if err != nil { + exitWithError(err) + } + signature := append(r.Bytes(), s.Bytes()...) + + tx.Vin[inID].Signature = signature + } +} + +// String returns a human-readable representation of a transaction +func (tx Transaction) String() string { + var lines []string + lines = append(lines, fmt.Sprintf("--- Transaction %x:", tx.ID)) + + for i, input := range tx.Vin { + lines = append(lines, fmt.Sprintf("\tInput %d:", i)) + lines = append(lines, fmt.Sprintf("\t\tTXID:\t%x", input.Txid)) + lines = append(lines, fmt.Sprintf("\t\tOut:\t%d", input.Vout)) + lines = append(lines, fmt.Sprintf("\t\tSignature:\t%x", input.Signature)) + lines = append(lines, fmt.Sprintf("\t\tPubKey:\t%x", input.PubKey)) + } + + for i, output := range tx.Vout { + lines = append(lines, fmt.Sprintf("\tOutput %d:", i)) + lines = append(lines, fmt.Sprintf("\t\tValue:\t%d", output.Value)) + lines = append(lines, fmt.Sprintf("\t\tScript:\t%x", output.PubKeyHash)) + } + + return strings.Join(lines, "\n") +} + +// TrimmedCopy creates a trimmed copy of Transaction to be used in signing +func (tx *Transaction) TrimmedCopy() Transaction { + var inputs []TXInput + var outputs []TXOutput + + for _, vin := range tx.Vin { + inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil}) + } + for _, vout := range tx.Vout { + outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash}) + } + txCopy := Transaction{tx.ID, inputs, outputs} + return txCopy +} + +// Verify verifies signatures of Transaction inputes +func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool { + txCopy := tx.TrimmedCopy() + curve := elliptic.P256() + + for inID, vin := range tx.Vin { + prevTx := prevTXs[hex.EncodeToString(vin.Txid)] + txCopy.Vin[inID].Signature = nil + txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash + txCopy.ID = txCopy.Hash() + txCopy.Vin[inID].PubKey = nil + + r := big.Int{} + s := big.Int{} + sigLen := len(vin.Signature) + r.SetBytes(vin.Signature[:(sigLen / 2)]) + s.SetBytes(vin.Signature[(sigLen / 2):]) + + x := big.Int{} + y := big.Int{} + keyLen := len(vin.PubKey) + x.SetBytes(vin.PubKey[:(keyLen / 2)]) + y.SetBytes(vin.PubKey[(keyLen / 2):]) + + rawPubKey := ecdsa.PublicKey{curve, &x, &y} + if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false { + return false + } + } + return true +} + // SetID sets the ID of a transaction func (tx Transaction) SetID() { var encoded bytes.Buffer @@ -43,18 +173,11 @@ func NewCoinbaseTX(to, data string) *Transaction { data = fmt.Sprintf("Reward to '%s'", to) } - txin := TXInput{ - Txid: []byte{}, - Vout: -1, - ScriptSig: data, - } - txout := TXOutput{ - Value: subsidy, - ScriptPubKey: to, - } + txin := TXInput{[]byte{}, -1, nil, []byte(data)} + txout := NewTXOutput(subsidy, to) - tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}} - tx.SetID() + tx := Transaction{nil, []TXInput{txin}, []TXOutput{*txout}} + tx.ID = tx.Hash() return &tx } @@ -64,75 +187,41 @@ func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transactio var inputs []TXInput var outputs []TXOutput - acc, validOutputs := bc.FindSpendableOutputs(from, amount) + wallets, err := NewWallets() + if err != nil { + exitWithError(err) + } + wallet := wallets.GetWallet(from) + pubKeyHash := HashPubKey(wallet.PublicKey) + acc, validOutputs := bc.FindSpendableOutputs(pubKeyHash, amount) if acc < amount { - fmt.Println("ERROR: Not enough funds") - os.Exit(1) + exitWithError(errors.New("Not enough funds")) } // Build a list of inputs for txid, outs := range validOutputs { txID, err := hex.DecodeString(txid) if err != nil { - fmt.Println(err) - os.Exit(1) + exitWithError(err) } for _, out := range outs { - input := TXInput{ - Txid: txID, - Vout: out, - ScriptSig: from, - } + input := TXInput{txID, out, nil, wallet.PublicKey} inputs = append(inputs, input) } } // Build a list of outputs - output := TXOutput{ - Value: amount, - ScriptPubKey: to, - } - outputs = append(outputs, output) + outputs = append(outputs, *NewTXOutput(amount, to)) if acc > amount { // There is some change - chgOutput := TXOutput{ - Value: acc - amount, - ScriptPubKey: from, - } - outputs = append(outputs, chgOutput) + outputs = append(outputs, *NewTXOutput(acc-amount, from)) } - tx := Transaction{ - ID: nil, - Vin: inputs, - Vout: outputs, - } - tx.SetID() + tx := Transaction{nil, inputs, outputs} + tx.ID = tx.Hash() + bc.SignTransaction(&tx, wallet.PrivateKey) return &tx } - -// TXInput represents a transaction input -type TXInput struct { - Txid []byte - Vout int - ScriptSig string -} - -// CanUnlockOutputWith checks whether the unlockingData (address) initiated the transaction -func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool { - return in.ScriptSig == unlockingData -} - -// TXOutput represents a transaction output -type TXOutput struct { - Value int - ScriptPubKey string -} - -// CanBeUnlockedWith checks if the output can be unlocked with the provided data -func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool { - return out.ScriptPubKey == unlockingData -} diff --git a/transaction_input.go b/transaction_input.go new file mode 100644 index 0000000..90eedc2 --- /dev/null +++ b/transaction_input.go @@ -0,0 +1,17 @@ +package main + +import "bytes" + +// TXInput represents a transaction input +type TXInput struct { + Txid []byte + Vout int + Signature []byte + PubKey []byte +} + +// UsesKey checks whether the address initiated the transaction +func (in *TXInput) UsesKey(pubKeyHash []byte) bool { + lockingHash := HashPubKey(in.PubKey) + return bytes.Compare(lockingHash, pubKeyHash) == 0 +} diff --git a/transaction_output.go b/transaction_output.go new file mode 100644 index 0000000..903d6f4 --- /dev/null +++ b/transaction_output.go @@ -0,0 +1,29 @@ +package main + +import "bytes" + +// TXOutput represents a transaction output +type TXOutput struct { + Value int + PubKeyHash []byte +} + +// newTXOutput creates a new output transaction +func NewTXOutput(value int, address string) *TXOutput { + txo := &TXOutput{value, nil} + txo.Lock([]byte(address)) + + return txo +} + +// Lock signs the output +func (out *TXOutput) Lock(address []byte) { + pubKeyHash := Base58Decode(address) + pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] + out.PubKeyHash = pubKeyHash +} + +// IsLockedWithKey checks if the output can be used by the owner of the pubkey +func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool { + return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0 +} diff --git a/utils.go b/utils.go index 5cdd349..a1d6c74 100644 --- a/utils.go +++ b/utils.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/binary" + "math/big" ) // IntToHex converts an int64 to a byte array @@ -14,3 +15,62 @@ func IntToHex(num int64) []byte { } return buf.Bytes() } + +// ReverseBytes reverses a byte array +func ReverseBytes(data []byte) { + for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { + data[i], data[j] = data[j], data[i] + } +} + +// Base58 Encode/Decode Functions +var b58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") + +// Base58Encode encodes a byte array to Base58 +func Base58Encode(input []byte) []byte { + var result []byte + x := big.NewInt(0).SetBytes(input) + + base := big.NewInt(int64(len(b58Alphabet))) + zero := big.NewInt(0) + mod := &big.Int{} + + for x.Cmp(zero) != 0 { + x.DivMod(x, base, mod) + result = append(result, b58Alphabet[mod.Int64()]) + } + + ReverseBytes(result) + for b := range input { + if b == 0x00 { + result = append([]byte{b58Alphabet[0]}, result...) + } else { + break + } + } + return result +} + +// Base58Decode decodes Base58-encoded data +func Base58Decode(input []byte) []byte { + result := big.NewInt(0) + zeroBytes := 0 + + for b := range input { + if b == 0x00 { + zeroBytes++ + } + } + + payload := input[zeroBytes:] + for _, b := range payload { + charIndex := bytes.IndexByte(b58Alphabet, b) + result.Mul(result, big.NewInt(58)) + result.Add(result, big.NewInt(int64(charIndex))) + } + + decoded := result.Bytes() + decoded = append(bytes.Repeat([]byte{byte(0x00)}, zeroBytes), decoded...) + + return decoded +} diff --git a/wallet.dat b/wallet.dat new file mode 100644 index 0000000..18cc16f Binary files /dev/null and b/wallet.dat differ diff --git a/wallet.go b/wallet.go new file mode 100644 index 0000000..75d6035 --- /dev/null +++ b/wallet.go @@ -0,0 +1,79 @@ +package main + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + + "golang.org/x/crypto/ripemd160" +) + +type Wallet struct { + PrivateKey ecdsa.PrivateKey + PublicKey []byte +} + +func NewWallet() *Wallet { + private, public := newKeyPair() + wallet := Wallet{ + PrivateKey: private, + PublicKey: public, + } + + return &wallet +} + +func (w Wallet) GetAddress() []byte { + pubKeyHash := HashPubKey(w.PublicKey) + + versionedPayload := append([]byte{version}, pubKeyHash...) + checksum := checksum(versionedPayload) + + fullPayload := append(versionedPayload, checksum...) + address := Base58Encode(fullPayload) + + return address +} + +func HashPubKey(pubKey []byte) []byte { + publicSHA256 := sha256.Sum256(pubKey) + + RIPEMD160Hasher := ripemd160.New() + _, err := RIPEMD160Hasher.Write(publicSHA256[:]) + if err != nil { + exitWithError(err) + } + publicRIPEMD160 := RIPEMD160Hasher.Sum(nil) + + return publicRIPEMD160 +} + +func ValidateAddress(address string) bool { + pubKeyHash := Base58Decode([]byte(address)) + actualChecksum := pubKeyHash[len(pubKeyHash)-addressChecksumLen:] + version := pubKeyHash[0] + pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-addressChecksumLen] + targetChecksum := checksum(append([]byte{version}, pubKeyHash...)) + + return bytes.Compare(actualChecksum, targetChecksum) == 0 +} + +func checksum(payload []byte) []byte { + firstSHA := sha256.Sum256(payload) + secondSHA := sha256.Sum256(firstSHA[:]) + + return secondSHA[:addressChecksumLen] +} + +func newKeyPair() (ecdsa.PrivateKey, []byte) { + curve := elliptic.P256() + private, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + exitWithError(err) + } + pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...) + + return *private, pubKey +} diff --git a/wallets.go b/wallets.go new file mode 100644 index 0000000..10af7cc --- /dev/null +++ b/wallets.go @@ -0,0 +1,93 @@ +package main + +import ( + "bytes" + "crypto/elliptic" + "encoding/gob" + "fmt" + "io/ioutil" + "os" +) + +const version = byte(0x00) +const walletFile = "wallet.dat" +const addressChecksumLen = 4 + +// Wallets is pretty self-explanatory +type Wallets struct { + Wallets map[string]*Wallet +} + +// NewWallets creates Wallets and fills it from a file if it exists +func NewWallets() (*Wallets, error) { + wallets := Wallets{} + wallets.Wallets = make(map[string]*Wallet) + + err := wallets.LoadFromFile() + + return &wallets, err +} + +// CreateWallet adds a Wallet to Wallets +func (ws *Wallets) CreateWallet() string { + wallet := NewWallet() + address := fmt.Sprintf("%s", wallet.GetAddress()) + + ws.Wallets[address] = wallet + + return address +} + +// GetAddresses returns an array of addresses stored in the wallet file +func (ws *Wallets) GetAddresses() []string { + var addresses []string + for address := range ws.Wallets { + addresses = append(addresses, address) + } + return addresses +} + +// GetWallet returns a wallet by its address +func (ws Wallets) GetWallet(address string) Wallet { + return *ws.Wallets[address] +} + +// LoadFromFile loads wallets from the file +func (ws *Wallets) LoadFromFile() error { + if _, err := os.Stat(walletFile); os.IsNotExist(err) { + return err + } + + fileContent, err := ioutil.ReadFile(walletFile) + if err != nil { + exitWithError(err) + } + + var wallets Wallets + gob.Register(elliptic.P256()) + decoder := gob.NewDecoder(bytes.NewReader(fileContent)) + err = decoder.Decode(&wallets) + if err != nil { + exitWithError(err) + } + ws.Wallets = wallets.Wallets + return nil +} + +// SaveToFile saves the wallets to a file +func (ws Wallets) SaveToFile() { + var content bytes.Buffer + + gob.Register(elliptic.P256()) + + encoder := gob.NewEncoder(&content) + err := encoder.Encode(ws) + if err != nil { + exitWithError(err) + } + + err = ioutil.WriteFile(walletFile, content.Bytes(), 0644) + if err != nil { + exitWithError(err) + } +}