Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bamorim committed Mar 14, 2018
0 parents commit 7a508e5
Show file tree
Hide file tree
Showing 22 changed files with 465 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
24 changes: 24 additions & 0 deletions .gitignore
@@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
bank-*.tar

1 change: 1 addition & 0 deletions .tool-versions
@@ -0,0 +1 @@
elixir 1.6.0
21 changes: 21 additions & 0 deletions README.md
@@ -0,0 +1,21 @@
# Bank

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `bank` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:bank, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/bank](https://hexdocs.pm/bank).

32 changes: 32 additions & 0 deletions config/config.exs
@@ -0,0 +1,32 @@
use Mix.Config

config :logger, level: String.to_atom((System.get_env "LOGLVL") || "error")


config :coins,
ecto_repos: [Coins.Repo]

config :coins, Coins.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "coins_readstore_dev",
hostname: "localhost",
port: 5432,
pool_size: 10

config :commanded,
event_store_adapter: Commanded.EventStore.Adapters.EventStore

# Configure the event store database
config :eventstore, EventStore.Storage,
serializer: EventStore.TermSerializer,
username: "postgres",
password: "postgres",
database: "coins_eventstore_dev",
hostname: "localhost",
port: 5432,
pool_size: 10

config :commanded_ecto_projections,
repo: Coins.Repo
39 changes: 39 additions & 0 deletions lib/coins.ex
@@ -0,0 +1,39 @@
defmodule Coins do
@moduledoc """
Documentation for Coins.
"""

alias Coins.{
Commands,
Transfer,
Repo,
Router,
Schemas
}

def mine_coin(account_id, nonce) do
%Commands.MineCoin{
account_id: account_id,
nonce: nonce
}
|> Router.dispatch
end

def send_coins(from, to, amount) do
%Commands.SendCoins{
account_id: from,
to: to,
amount: amount,
transfer_id: UUID.uuid4
}
|> Router.dispatch
end

def richest do
import Ecto.Query
Schemas.Account
|> order_by(desc: :balance)
|> limit(1)
|> Repo.one
end
end
68 changes: 68 additions & 0 deletions lib/coins/account.ex
@@ -0,0 +1,68 @@
defmodule Coins.Account do
alias __MODULE__

alias Coins.Commands.{
MineCoin,
ReceiveCoins,
SendCoins
}

alias Coins.Events.{
CoinMined,
CoinsSent,
CoinsReceived
}

defstruct [balance: 0, last_nonce: 0]

def execute(%{last_nonce: ln}, %MineCoin{nonce: n}) when ln >= n,
do: {:error, :used_nonce}

def execute(_, %MineCoin{} = cmd) do
if Proof.proof(cmd.account_id, cmd.nonce) do
%CoinMined{
account_id: cmd.account_id,
nonce: cmd.nonce
}
else
{:error, :invalid_nonce}
end
end

def execute(%{balance: b}, %SendCoins{amount: a}) when a > b,
do: {:error, :not_enough_coins}

def execute(_, %SendCoins{} = cmd) do
%CoinsSent{
account_id: cmd.account_id,
to: cmd.to,
amount: cmd.amount,
transfer_id: cmd.transfer_id
}
end

def execute(_, %ReceiveCoins{} = cmd) do
%CoinsReceived{
account_id: cmd.account_id,
amount: cmd.amount,
transfer_id: cmd.transfer_id
}
end

def apply(state, %CoinMined{} = evt) do
%Account{ state | last_nonce: evt.nonce }
|> increase_balance(1)
end

def apply(state, %CoinsSent{} = evt) do
increase_balance(state, -evt.amount)
end

def apply(state, %CoinsReceived{} = evt) do
increase_balance(state, evt.amount)
end

defp increase_balance(state, amount) do
%Account{state | balance: state.balance + amount}
end
end
29 changes: 29 additions & 0 deletions lib/coins/account_projector.ex
@@ -0,0 +1,29 @@
defmodule Coins.AccountProjector do
use Commanded.Projections.Ecto,
name: "AccountProjector"

alias Coins.Events, as: E
alias Coins.Schemas, as: S

project %E.CoinMined{} = evt do
increase_balance(multi, evt.account_id, 1)
end

project %E.CoinsSent{} = evt do
increase_balance(multi, evt.account_id, -evt.amount)
end

project %E.CoinsReceived{} = evt do
increase_balance(multi, evt.account_id, evt.amount)
end

defp increase_balance(multi, account_id, amount) do
Ecto.Multi.insert(
multi,
:increase_balance,
%S.Account{account_id: account_id, balance: amount},
conflict_target: :account_id,
on_conflict: [inc: [balance: amount]]
)
end
end
9 changes: 9 additions & 0 deletions lib/coins/application.ex
@@ -0,0 +1,9 @@
defmodule Coins.Application do
@moduledoc false

use Application

def start(_type, args) do
Coins.Supervisor.start_link(args)
end
end
25 changes: 25 additions & 0 deletions lib/coins/commands.ex
@@ -0,0 +1,25 @@
defmodule Coins.Commands do
defmodule MineCoin do
defstruct [
:account_id,
:nonce
]
end

defmodule SendCoins do
defstruct [
:account_id,
:to,
:amount,
:transfer_id
]
end

defmodule ReceiveCoins do
defstruct [
:account_id,
:amount,
:transfer_id
]
end
end
25 changes: 25 additions & 0 deletions lib/coins/events.ex
@@ -0,0 +1,25 @@
defmodule Coins.Events do
defmodule CoinMined do
defstruct [
:account_id,
:nonce
]
end

defmodule CoinsSent do
defstruct [
:account_id,
:to,
:amount,
:transfer_id
]
end

defmodule CoinsReceived do
defstruct [
:account_id,
:amount,
:transfer_id
]
end
end
3 changes: 3 additions & 0 deletions lib/coins/repo.ex
@@ -0,0 +1,3 @@
defmodule Coins.Repo do
use Ecto.Repo, otp_app: :coins
end
16 changes: 16 additions & 0 deletions lib/coins/router.ex
@@ -0,0 +1,16 @@
defmodule Coins.Router do
use Commanded.Commands.Router

alias Coins.Account
alias Coins.Commands, as: C

dispatch(
[
C.MineCoin,
C.SendCoins,
C.ReceiveCoins
],
to: Account,
identity: :account_id
)
end
10 changes: 10 additions & 0 deletions lib/coins/schemas/account.ex
@@ -0,0 +1,10 @@
defmodule Coins.Schemas.Account do
use Ecto.Schema

@primary_key false

schema "accounts" do
field(:account_id, :string)
field(:balance, :integer)
end
end
28 changes: 28 additions & 0 deletions lib/coins/send_coins_process.ex
@@ -0,0 +1,28 @@
defmodule Coins.SendCoinsProcess do
use Commanded.ProcessManagers.ProcessManager,
name: "SendCoinsProcess",
router: Coins.Router

alias Coins.Commands, as: C
alias Coins.Events, as: E

defstruct []

def interested?(%E.CoinsSent{transfer_id: id}) do
{:start, id}
end

def interested?(%E.CoinsReceived{transfer_id: id}) do
{:stop, id}
end

def handle(_, %E.CoinsSent{} = evt) do
%C.ReceiveCoins{
transfer_id: evt.transfer_id,
account_id: evt.to,
amount: evt.amount
}
end

def apply(state, _event), do: state
end
19 changes: 19 additions & 0 deletions lib/coins/supervisor.ex
@@ -0,0 +1,19 @@
defmodule Coins.Supervisor do
@moduledoc false

use Supervisor

def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end

def init(_arg) do
children = [
{Coins.Repo, []},
{Coins.AccountProjector, []},
{Coins.SendCoinsProcess, []},
]

Supervisor.init(children, strategy: :one_for_one)
end
end
25 changes: 25 additions & 0 deletions lib/proof.ex
@@ -0,0 +1,25 @@
defmodule Proof do
# Just to help find the valid nonces
def nonces(string, amount, difficulty \\ 1) do
1
|> Stream.iterate(&(&1 + 1))
|> Stream.filter(&(proof(string, &1, difficulty)))
|> Enum.take(amount)
end

def proof(string, nonce, difficulty \\ 1) do
String.starts_with?(
hash( hash(string) <> hash(to_string(nonce)) ),
header(difficulty)
)
end

defp header(difficulty) do
[<<0>>]
|> Stream.cycle
|> Enum.take(difficulty)
|> Enum.join("")
end

defp hash(x), do: :crypto.hash(:sha256, x)
end

0 comments on commit 7a508e5

Please sign in to comment.