Skip to content

Commit

Permalink
Create ETHTransactions module
Browse files Browse the repository at this point in the history
  • Loading branch information
tteerawat committed Apr 17, 2021
1 parent 18b8473 commit e19aa96
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 11 deletions.
1 change: 1 addition & 0 deletions .iex.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
alias Satana.ETHTransactions
27 changes: 16 additions & 11 deletions lib/satana/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,14 @@ defmodule Satana.Application do

use Application

def start(_type, _args) do
children = [
# Start the Telemetry supervisor
SatanaWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Satana.PubSub},
# Start the Endpoint (http/https)
SatanaWeb.Endpoint
# Start a worker by calling: Satana.Worker.start_link(arg)
# {Satana.Worker, arg}
]
def start(_type, args) do
children =
[
SatanaWeb.Telemetry,
{Phoenix.PubSub, name: Satana.PubSub},
SatanaWeb.Endpoint,
{Finch, name: Satana.Finch}
] ++ list_children_by_env(args[:env])

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
Expand All @@ -29,4 +26,12 @@ defmodule Satana.Application do
SatanaWeb.Endpoint.config_change(changed, removed)
:ok
end

defp list_children_by_env(:test) do
[]
end

defp list_children_by_env(_) do
[Satana.ETHTransactions.Store]
end
end
52 changes: 52 additions & 0 deletions lib/satana/eth_transactions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule Satana.ETHTransactions do
alias Satana.Blocknative
alias Satana.ETHTransactions.Store
alias Satana.ETHTransactions.Transaction
alias Satana.Slack

require Logger

@spec list_transactions_by_status(String.t()) :: [Transaction.t()]
def list_transactions_by_status(status) do
Store.list_transactions()
|> Enum.filter(&(&1.status == status))
end

@spec add_eth_transaction(String.t()) :: :ok | {:error, String.t()}
def add_eth_transaction(tx_id) do
with :ok <- Store.add_transaction(tx_id, handle_transaction_in: {2, :minutes}, with: &handle_transaction/1),
:ok <- Blocknative.add_eth_transaction_to_watch(tx_id) do
text = "Transaction `#{tx_id}` has been registered :sunglasses:"
Slack.send_message(text)

:ok
else
{:error, :already_exists} ->
{:error, "transaction #{tx_id} already exists"}

{:error, msg} ->
Logger.warn(fn -> "Rollback transaction #{tx_id} - #{msg}" end)

Store.delete_transaction(tx_id)

{:error, msg}
end
end

defp handle_transaction(%Transaction{} = transaction) do
if Transaction.pending?(transaction) do
text = "It's been a while but transaction `#{transaction.tx_id}` is still pending :thinking_face:"
Slack.send_message(text)
end
end

@spec confirm_transaction!(String.t()) :: :ok
def confirm_transaction!(tx_id) do
:ok = Store.update_transaction(tx_id, &Transaction.confirm/1)

text = "Transaction `#{tx_id}` has been confirmed :tada:"
Slack.send_message(text)

:ok
end
end
95 changes: 95 additions & 0 deletions lib/satana/eth_transactions/store.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
defmodule Satana.ETHTransactions.Store do
use GenServer

alias Satana.ETHTransactions.Transaction

require Logger

@spec start_link(Keyword.t()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end

@spec add_transaction(String.t(), keyword()) :: :ok | {:error, :already_exists}
def add_transaction(tx_id, handle_transaction_in: {num, unit}, with: callback_function)
when unit in [:seconds, :minutes] and is_function(callback_function, 1) do
GenServer.call(__MODULE__, {:add_new_transaction, tx_id, {num, unit}, callback_function})
end

@spec delete_transaction(String.t()) :: :ok
def delete_transaction(tx_id) do
GenServer.call(__MODULE__, {:delete_transaction, tx_id})
end

@spec update_transaction(String.t(), fun()) :: :ok | {:error, :not_found}
def update_transaction(tx_id, update_function) when is_function(update_function, 1) do
GenServer.call(__MODULE__, {:update_transaction, tx_id, update_function})
end

@spec list_transactions :: [Transaction.t()]
def list_transactions do
GenServer.call(__MODULE__, :list_transactions)
end

## GenServer callbacks

@impl GenServer
def init(opts) do
Logger.info(fn -> "Running #{__MODULE__}" end)
initial_state = opts[:initial_state] || []
{:ok, initial_state}
end

@impl GenServer
def handle_call({:add_new_transaction, tx_id, {num, unit}, callback_function}, _from, current_transactions) do
if Enum.find(current_transactions, &(&1.tx_id == tx_id)) do
{:reply, {:error, :already_exists}, current_transactions}
else
time = apply(:timer, unit, [num])
Process.send_after(self(), {:check_transaction_status, tx_id, callback_function}, time)
new_transaction = Transaction.new(tx_id)

{:reply, :ok, [new_transaction | current_transactions]}
end
end

@impl GenServer
def handle_call({:delete_transaction, tx_id}, _from, current_transactions) do
index = Enum.find_index(current_transactions, &(&1.tx_id == tx_id))

if index do
updated_transactions = List.delete_at(current_transactions, index)

{:reply, :ok, updated_transactions}
else
{:reply, :ok, current_transactions}
end
end

@impl GenServer
def handle_call({:update_transaction, tx_id, update_function}, _from, current_transactions) do
index = Enum.find_index(current_transactions, &(&1.tx_id == tx_id))

if index do
updated_transactions = List.update_at(current_transactions, index, update_function)

{:reply, :ok, updated_transactions}
else
{:reply, {:error, :not_found}, current_transactions}
end
end

@impl GenServer
def handle_call(:list_transactions, _from, transactions) do
{:reply, transactions, transactions}
end

@impl GenServer
def handle_info({:check_transaction_status, tx_id, callback_function}, transactions) do
transaction = Enum.find(transactions, &(&1.tx_id == tx_id))

if transaction, do: callback_function.(transaction)

{:noreply, transactions}
end
end
28 changes: 28 additions & 0 deletions lib/satana/eth_transactions/transaction.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule Satana.ETHTransactions.Transaction do
@enforce_keys [:tx_id, :status]

defstruct @enforce_keys

@type t :: %__MODULE__{
tx_id: String.t(),
status: String.t()
}

@spec new(String.t()) :: t()
def new(tx_id) do
%__MODULE__{
tx_id: tx_id,
status: "pending"
}
end

@spec confirm(t()) :: t()
def confirm(%__MODULE__{} = transaction) do
%{transaction | status: "confirmed"}
end

@spec pending?(t()) :: boolean()
def pending?(%__MODULE__{status: status}) do
status == "pending"
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ defmodule Satana.MixProject do
{:bypass, "~> 2.1", only: :test},
{:finch, "~> 0.6"},
{:jason, "~> 1.0"},
{:mimic, "~> 1.5", only: :test},
{:phoenix, "~> 1.5.8"},
{:plug_cowboy, "~> 2.0"},
{:telemetry_metrics, "~> 0.4"},
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"finch": {:hex, :finch, "0.6.3", "18b993653f5d7d5550b0a3c3f9777269b2b99db02726ac6fe776d58c2dd1a0a0", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.2", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "015f48d3a43f9d2afd06ef714636545bdb017297a63bf582cac8fdcf8ae6f031"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimic": {:hex, :mimic, "1.5.0", "ad33740f414412639006afa4092f7bf09f5ddeb51999f94181ff8ad9e5d5d675", [:mix], [], "hexpm", "0f4620b88623c3869a11bd7e5691cccc712290c4bcbf73bf07bfb37c3c6932a4"},
"mint": {:hex, :mint, "1.2.1", "369cc8fecc54afd170e11740aa7efd066709e5ef3b5a2c63f0a47d1542cbd56a", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "053fe2f48c965f31878a16272478d9299fa412bc4df86dee2678986f2e40e018"},
"nimble_options": {:hex, :nimble_options, "0.3.5", "a4f6820cdcb4ee444afd78635f323e58e8a5ddf2fbbe9b9d283a99f972034bae", [:mix], [], "hexpm", "f5507cc90033a8d12769522009c80aa9164af6bab245dbd4ad421d008455f1e1"},
"nimble_pool": {:hex, :nimble_pool, "0.2.4", "1db8e9f8a53d967d595e0b32a17030cdb6c0dc4a451b8ac787bf601d3f7704c3", [:mix], [], "hexpm", "367e8071e137b787764e6a9992ccb57b276dc2282535f767a07d881951ebeac6"},
Expand Down
97 changes: 97 additions & 0 deletions test/satana/eth_transactions/store_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
defmodule Satana.ETHTransactions.StoreTest do
use ExUnit.Case

alias Satana.ETHTransactions.Store
alias Satana.ETHTransactions.Transaction

defmodule Messenger do
require Logger

def send_message(text) do
Logger.info(text)
end
end

setup do
start_supervised!(Store)

:ok
end

test "behaves as expected" do
{:ok, agent_pid} = Agent.start_link(fn -> [] end)

tx_id1 = "0x1ded29a6bc74abbcc51313fc261b2f97c7f1fc5b9adbbdbaca00ac959f519e7b"
tx_id2 = "0xeb84a81a973187ff9d79934723eb801c400ed941a2a34215ddb202db152e4b84"
tx_id3 = "0x58c9efa287cedee606bfe264898fc24b22ca4e824883585962d1d0ad1140dfd4"

callback_function = fn %Transaction{} = transaction ->
if Transaction.pending?(transaction) do
Agent.update(agent_pid, fn state -> [transaction.tx_id | state] end)
end
end

# check initial state
assert Store.list_transactions() == []

# add some transactions
assert Store.add_transaction(tx_id1,
handle_transaction_in: {2, :seconds},
with: callback_function
) == :ok

assert Store.add_transaction(tx_id2,
handle_transaction_in: {2, :seconds},
with: callback_function
) == :ok

assert Store.add_transaction(tx_id3,
handle_transaction_in: {2, :seconds},
with: callback_function
) == :ok

assert Store.add_transaction(tx_id1,
handle_transaction_in: {2, :seconds},
with: callback_function
) == {:error, :already_exists}

# check state immediately after transactions added
assert Store.list_transactions() == [
%Transaction{
tx_id: tx_id3,
status: "pending"
},
%Transaction{
tx_id: tx_id2,
status: "pending"
},
%Transaction{
tx_id: tx_id1,
status: "pending"
}
]

# modify state
assert Store.update_transaction(tx_id1, &Transaction.confirm/1) == :ok
assert Store.update_transaction("0x123", &Transaction.confirm/1) == {:error, :not_found}
assert Store.delete_transaction(tx_id3) == :ok

# wait for callback function to be called
:timer.sleep(2_500)

# check current state after state modification
assert Store.list_transactions() == [
%Transaction{
tx_id: tx_id2,
status: "pending"
},
%Transaction{
tx_id: tx_id1,
status: "confirmed"
}
]

# check if callback function is called
assert Agent.get(agent_pid, & &1) == [tx_id2]
end
end
Loading

0 comments on commit e19aa96

Please sign in to comment.