defmodule Forge.Wallet do @moduledoc """ Wallet client """ require Logger alias Forge.{AccountState, Crypto, Helper, Rpc, Server, Tx.TransferTransaction} @typedoc """ Ethereum-compatible wallet. It contains keys as folows: * `:private_key`: Base16 encoded private key. * `:public_key`: Base16 encoded public key. * `:address`: wallet address. """ @type t :: %{ private_key: String.t(), public_key: String.t(), address: String.t() } @doc """ Create a new eth-like wallet """ @spec new() :: t() def new do private_key = Crypto.create_private_key() public_key = Crypto.create_public_key(private_key) address = Crypto.create_wallet_address(public_key) %{ private_key: Base.encode16(private_key), public_key: Base.encode16(public_key), address: address } end @spec save(t(), binary()) :: :ok def save(wallet, passphrase) do path = Helper.get_key_store_path() File.mkdir_p!(path) filename = Path.join(path, wallet.address) [iv: iv, ciphertext: cipher] = Crypto.block_encrypt(wallet.private_key, passphrase) data = Map.merge(wallet, %{private_key: cipher, iv: Base.encode64(iv)}) File.write!(filename, Jason.encode!(data)) end @spec load(binary() | map() | any(), binary()) :: :error | t() def load(address, passphrase) when is_binary(address) do path = Helper.get_key_store_path() filename = Path.join(path, address) case File.exists?(filename) do true -> content = filename |> File.read!() |> Jason.decode!(keys: :atoms!) load(content, passphrase) _ -> :error end end def load(content, passphrase) when is_map(content) do %{address: address, private_key: cipher, public_key: public_key, iv: iv} = content private_key = Crypto.block_decrypt(cipher, passphrase, Base.decode64!(iv)) case private_key do :error -> :error _ -> %{ private_key: private_key, public_key: public_key, address: address } end rescue e -> Logger.error(e) :error end def load(_, _), do: :error @doc """ Transfer token from one address to another """ @spec transfer(t(), String.t(), non_neg_integer()) :: :ok | {:error, any} def transfer(%{address: from, public_key: public_key, private_key: private_key}, to, total) do nonce = nonce(from) from |> TransferTransaction.create(to, nonce, total, public_key, private_key) |> Rpc.send() end @doc """ Retrieve the current balance of the address """ @spec balance(String.t()) :: non_neg_integer() def balance(address) do apply(Server, :account_balance, [address]) end @doc """ Retrieve the current nonce of the address """ @spec nonce(String.t()) :: non_neg_integer() def nonce(address) do apply(Server, :account_nonce, [address]) end @doc """ Retrieve the account info of the address """ @spec info(String.t(), String.t() | non_neg_integer()) :: AccountState.t() | nil def info(address, app_hash \\ "") do apply(Server, :account_info, [address, app_hash]) end @doc """ Retrieve the chain info """ @spec chain_info :: map() def chain_info do apply(Server, :chain_info, []) end end