Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : transaction.merge endpoint #1715

Merged
merged 169 commits into from
Nov 2, 2020
Merged
Show file tree
Hide file tree
Changes from 168 commits
Commits
Show all changes
169 commits
Select commit Hold shift + click to select a range
1ab5d4b
test: Init
Aug 5, 2020
8cee04f
test: create `generate_utxos_map/3`
Aug 5, 2020
3d8cd23
test: put test in describe
Aug 5, 2020
db4e77e
test: added `needed_funds/2` tests
Aug 5, 2020
0ed41d7
add tests and light refactor for get_sorted_grouped_utxos
Aug 5, 2020
9379147
add tests and light refactor [2]
Aug 5, 2020
aa01847
test: refactor `create_advice/2`
Aug 5, 2020
9fb398f
add spec and clarify funds_sufficient
Aug 5, 2020
8fe68f0
refactor: `needed_funds`
Aug 5, 2020
eb61669
Merge branches 'ripzery/optimize-transaction-create' and 'ripzery/opt…
Aug 5, 2020
3e47a08
refactor: use shorthanded type
Aug 5, 2020
816465a
Add @doc and readabilty edits
Aug 6, 2020
cb35988
add: tests for funds_sufficient/1
Aug 6, 2020
3f0fdfd
refactor: simplify use of 'alice'
Aug 6, 2020
f433eb4
refactor: move test inside describe
Aug 6, 2020
4c1a713
test: add `select_utxo/2` tests
Aug 6, 2020
88abe16
refactor: remove extra line
Aug 6, 2020
ef8b4da
fix: spec
Aug 7, 2020
bde05f2
Merge branch 'master' of https://github.com/omisego/elixir-omg into r…
Aug 10, 2020
36789fc
feat: implement `add_merge_utxos`
Aug 10, 2020
9ab4f39
test: add test for `add_merge_utxos/2`
Aug 10, 2020
02aa0de
feat: add prioritize_merge_utxos/2
Aug 10, 2020
5c164a8
test: add test for `prioritize_merge_utxos/2`
Aug 10, 2020
c08deea
refactor: pipe one more time
Aug 11, 2020
2474b85
refactor and spec stealth merge
Aug 11, 2020
61d1fce
refactor: test name
Aug 11, 2020
85deb42
Merge branch 'ripzery/optimize-transaction-create' of github.com:omgn…
Aug 11, 2020
dd86791
add min_length to base validators
nicholasmueller Aug 11, 2020
4e439e9
add merge route and controller
nicholasmueller Aug 11, 2020
2ebe660
fix compile errors
nicholasmueller Aug 11, 2020
4cef457
add: tests and doc for `add_utxos_for_stealth_merge`
Aug 11, 2020
0a3100d
test address/currency and utxo positions constraint
nicholasmueller Aug 11, 2020
5eb4ee4
feat: integrate add_merge_utxos to create_advice
Aug 11, 2020
692cdbc
test: add test for create_advice
Aug 11, 2020
a64aba8
Merge conflict
Aug 11, 2020
4b91019
add: doc content for add_utxos_for_stealth_merge
Aug 13, 2020
6cbcdbc
Merge branch 'ripzery/optimize-transaction-create' of https://github.…
Aug 13, 2020
6b268a6
refactor and test prioritisation of UTXOs for stealth merge
Aug 13, 2020
e6abf77
test utxo constraint
nicholasmueller Aug 13, 2020
50730b8
add other required utxo data
nicholasmueller Aug 13, 2020
fd17e79
fetch utxo logic
nicholasmueller Aug 13, 2020
908db37
start transaction_test
nicholasmueller Aug 14, 2020
528f86e
finaly get basic transaction api test working
nicholasmueller Aug 14, 2020
48861f1
merge p
nicholasmueller Aug 14, 2020
12e6680
refactor: split transaction operation
Aug 14, 2020
bb1f5f8
Fix dialyzer type
Aug 16, 2020
2a96ec0
refactor: dialyzer
Aug 18, 2020
9089a0e
fix: typo
Aug 18, 2020
dd0dec1
test: add utxo_selection test
Aug 18, 2020
412dfd2
debug: transaction create
Aug 18, 2020
bfaae7a
feat: handle when original inputs are empty
Aug 18, 2020
e419ab8
tests: remove transaction merge tests
Aug 18, 2020
22ea3d7
Merge branch 'master' into ripzery/optimize-transaction-create
Aug 18, 2020
85ec55d
tests: add tests for utxo selection
Aug 18, 2020
08a3345
tests: handle when too many inputs to satisfy payments and fee
Aug 18, 2020
32b0c6d
refactor: remove todo
Aug 18, 2020
97f5b0c
refactor: parameter validation for transaction.merge
Aug 19, 2020
c1c8f8d
Merge branch 'ripzery/optimize-transaction-create' of https://github.…
Aug 21, 2020
aaade0b
Merge branch 'master' of https://github.com/omisego/elixir-omg into n…
Aug 21, 2020
4df2ddb
optional desc/asc for UTXO fetch
Aug 21, 2020
ef95ea2
WIP: merge functionality
Aug 21, 2020
7c07d83
feat: move `create_advice` from `utxo_selection.ex` to `transaction.…
Aug 21, 2020
1c2a4e4
test: add transaction tests
Aug 21, 2020
e2123c2
WIP: return multiple merge transactions if address/ccy params
Aug 21, 2020
f90b2f7
Merge branch 'ripzery/optimize-transaction-create' of https://github.…
Aug 21, 2020
cc6427c
test: change port `9657` to `9656`
Aug 21, 2020
4994fa5
cleanup: remove unused var
Aug 21, 2020
26448df
lint: remove @doc for defp
Aug 21, 2020
a7689a5
test: revert back port `9656` to `9657`
Aug 21, 2020
37820e4
lint: fix lint
Aug 21, 2020
97f3588
refactor: remove result from response
Aug 21, 2020
ed48bef
Merge branch 'master' of https://github.com/omisego/elixir-omg into n…
Aug 24, 2020
ab5de0d
Merge branch 'ripzery/optimize-transaction-create' of https://github.…
Aug 24, 2020
758a2cb
test: add prepare_test_server
Aug 24, 2020
b96787c
WIP: merge when given utxo positions
Aug 24, 2020
898ef00
refactor: fix per reviews
Aug 24, 2020
8189f7a
tests: add test case where 2 input use different currencies
Aug 24, 2020
135d928
refactor: remove alice, bob from test context
Aug 24, 2020
2b2db83
test: fix mismatch function signature
Aug 24, 2020
f90dbcc
fix: dialyzer type
Aug 24, 2020
625c130
test for merge with address and currency, refactor create merge so no…
nicholasmueller Aug 24, 2020
7b0eda1
more happy path tests
nicholasmueller Aug 24, 2020
eb607d0
refactor: Cleanup
Aug 24, 2020
ae0ebe9
fix: dialyzer
Aug 25, 2020
ada001c
fix: credo
Aug 25, 2020
89f664d
remove unusued alias
Aug 25, 2020
0b5d31d
remove unaccepted merge constraints
nicholasmueller Aug 26, 2020
2be71ee
remove max 4 merge constratint
nicholasmueller Aug 26, 2020
652e1df
match tx.create response shape
nicholasmueller Aug 26, 2020
8aa0e93
refactor: remove comments
Aug 26, 2020
6a95742
lint: fix
Aug 26, 2020
7d253e7
tests: add tests for transaction.create apis
Aug 26, 2020
78daf72
test: delete unnecessary function
Aug 26, 2020
be1ca39
address/currency merge controller tests
nicholasmueller Aug 26, 2020
8bc1973
utxo position happy path
nicholasmueller Aug 26, 2020
5bbd8b9
add multiple tx for utxo pos test
nicholasmueller Aug 26, 2020
374ac55
Merge branch 'master' into ripzery/optimize-transaction-create
Aug 26, 2020
0e6843a
refactor: simplified `select_inputs` flow
Aug 26, 2020
2dedd6f
start merge view test
nicholasmueller Aug 27, 2020
3aee87d
revert merge view since controler test makes this redundant
nicholasmueller Aug 27, 2020
a0f2a77
Merge branch 'master' into nm-transaction-merge-endpoint
nicholasmueller Aug 27, 2020
69b95ff
Merge branch 'ripzery/optimize-transaction-create' into nm-transactio…
nicholasmueller Aug 27, 2020
6be39d4
remove default argument in get_sorted_grouped_utxos and reflect change
Aug 27, 2020
0b652c2
refactor: remove "respond"
Aug 27, 2020
e055ceb
update swagger for merge endpoint
nicholasmueller Aug 27, 2020
aac5515
generate auto swagger docs
nicholasmueller Aug 27, 2020
20a3ffc
move private below public funcs
nicholasmueller Aug 27, 2020
73aa38a
change list first for match
nicholasmueller Aug 27, 2020
ca32f4c
refactor: fix follows pr review
Aug 27, 2020
f96403d
tests: fix wrong function
Aug 27, 2020
7a0ee66
lint: add alias
Aug 27, 2020
6bfb498
refactor: remove trailing comma
Aug 27, 2020
83df000
refactor: var name
Aug 27, 2020
a64f699
fixes: tests, dialyzer, credo
Aug 28, 2020
3ff17c3
remove single pipes
Aug 28, 2020
0e2e21a
docs: two examples for transaction.merge
Aug 28, 2020
3cbcf11
Merge branch 'ripzery/optimize-transaction-create' of https://github.…
Aug 28, 2020
d236051
switch branch for cabbage test
Aug 28, 2020
a80a2a8
refactor: move functions out of controller
Aug 30, 2020
08428c5
Merge branch 'ripzery/optimize-transaction-create' of github.com:omgn…
Aug 31, 2020
211cd59
test: add outputs to assertion
Aug 31, 2020
7f7b59b
test: add tests for `transaction.create/2`
Aug 31, 2020
c6ed37d
test: implement tests for include_typed_data/1
Aug 31, 2020
f85a70a
refactor: remove single pipe
Aug 31, 2020
c3d4891
tests: add tests for `select_inputs/2`
Sep 1, 2020
c8bcd40
refactor: cleanup typespec
Sep 1, 2020
6b1f10a
lint: formatting
Sep 1, 2020
c4e7ec7
lint: remove unused alias
Sep 1, 2020
f4b6461
Merge branch 'master' into ripzery/optimize-transaction-create
Sep 1, 2020
03d9714
lint: reorder the alias
Sep 1, 2020
cc89117
Merge branch 'ripzery/optimize-transaction-create' of github.com:omgn…
Sep 1, 2020
77773b0
refactor: move spec and simplified logic
Sep 1, 2020
11d9ca4
refactor: simplify pattern matching
Sep 1, 2020
0c33028
refactor: uniformize param
Sep 1, 2020
6babc77
Merge branch 'ripzery/optimize-transaction-create' into nm-transactio…
nicholasmueller Sep 2, 2020
42f03e0
revert: bring back merge transaction
Sep 2, 2020
9d0be6b
fixes
Sep 2, 2020
cd7ecda
refactor: lift out respond
Sep 2, 2020
f46ce14
Merge branch 'ripzery/optimize-transaction-create' of github.com:omgn…
Sep 2, 2020
b81d83a
Merge branch 'ripzery/optimize-transaction-create' of https://github.…
Sep 2, 2020
175bddf
fix: is_required_merge doesn't work
Sep 2, 2020
d8c1fc1
refactor: transaction api
Sep 2, 2020
4c5e040
Merge branch 'ripzery/optimize-transaction-create' of https://github.…
Sep 2, 2020
4b0709c
resolve initial conflicts
Sep 2, 2020
f635fc4
tighten specification and refactor tests
Sep 3, 2020
1da9055
add: dialyzer specs
Sep 3, 2020
a16390e
refactor: merge constraints
Sep 3, 2020
547faac
refactor: accept max. 4 utxo positions
Sep 8, 2020
1b80e3b
refactor/add tests
Sep 8, 2020
920e7ed
refactor: merge_constraints limit max. positions to 4
Sep 8, 2020
df1650c
refactor: replace Alice and Bob fixtures with setup
Sep 8, 2020
a47e76d
refactor: return keyword lists from parameter validation
Sep 8, 2020
6e70f62
update documentation
Sep 9, 2020
d60473e
Merge branch 'master' of https://github.com/omisego/elixir-omg into n…
Oct 7, 2020
dd4ebc3
Merge branch 'master' of https://github.com/omisego/elixir-omg into n…
Oct 20, 2020
4bac755
Merge branch 'master' of https://github.com/omisego/elixir-omg into n…
Oct 21, 2020
e6988a7
post-merge: fix failing tests
Oct 21, 2020
ed73ccf
post-merge: fix failing tests
Oct 21, 2020
033869f
fix: dialyzer
Oct 21, 2020
b4b087d
fix (lint): remove unused aliases
Oct 21, 2020
4ccfcea
post-merge: remove unintended spacing
Oct 21, 2020
e5a7315
add: fallback descriptions
Oct 21, 2020
84d1dd9
remove `result` key from body schema for /merge
Oct 21, 2020
5f1c11b
reinstate result key where removed.
Oct 21, 2020
ece9a5a
rename tests
Oct 21, 2020
3c6ed93
fix: duplicate test typo
Oct 21, 2020
b28c836
Merge branch 'master' of https://github.com/omisego/elixir-omg into n…
Oct 22, 2020
cde2109
remove empty line
Oct 22, 2020
56387a7
Merge branch 'master' of https://github.com/omisego/elixir-omg into n…
Oct 30, 2020
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
6 changes: 6 additions & 0 deletions apps/omg_utils/lib/omg_utils/http_rpc/validators/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule OMG.Utils.HttpRPC.Validator.Base do
# IMPORTANT: Alias can use already defined validators, not other aliases (no backtracking)
@aliases %{
address: [:hex, length: 20],
currency: [:hex, length: 20],
hash: [:hex, length: 32],
signature: [:hex, length: 65],
pos_integer: [:integer, greater: 0],
Expand Down Expand Up @@ -139,6 +140,11 @@ defmodule OMG.Utils.HttpRPC.Validator.Base do
def max_length({list, []}, len) when is_list(list) and length(list) <= len, do: {list, []}
def max_length({val, []}, len), do: {val, max_length: len}

@spec min_length({any(), list()}, non_neg_integer()) :: {any(), list()}
def min_length({_, [_ | _]} = err, _len), do: err
def min_length({list, []}, len) when is_list(list) and length(list) >= len, do: {list, []}
def min_length({val, []}, len), do: {val, min_length: len}
Comment on lines +143 to +146
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wowowowo, nice!


@spec greater({any(), list()}, integer()) :: {any(), list()}
def greater({_, [_ | _]} = err, _b), do: err
def greater({val, []}, bound) when is_integer(val) and val > bound, do: {val, []}
Expand Down
13 changes: 12 additions & 1 deletion apps/omg_utils/test/omg_utils/http_rpc/validators/base_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ defmodule OMG.Utils.HttpRPC.Validator.BaseTest do
"too_short_hash" => "0x" <> String.duplicate("00", 31),
"len_1" => "1",
"len_2" => <<1, 2, 3, 4, 5>>,
"max_len_1" => [1, 2, 3, 4, 5]
"max_len_1" => [1, 2, 3, 4, 5],
"min_len_1" => [1, 2, 3]
}

describe "Basic validation:" do
Expand Down Expand Up @@ -121,6 +122,16 @@ defmodule OMG.Utils.HttpRPC.Validator.BaseTest do
assert {:error, {:validation_error, "max_len_1", {:max_length, 3}}} == expect(@params, "max_len_1", max_length: 3)
end

test "min_length: positive cases" do
assert {:ok, [1, 2, 3]} == expect(@params, "min_len_1", min_length: 0)
assert {:ok, [1, 2, 3]} == expect(@params, "min_len_1", min_length: 2)
assert {:ok, [1, 2, 3]} == expect(@params, "min_len_1", min_length: 3)
end

test "min_length: negative cases" do
assert {:error, {:validation_error, "min_len_1", {:min_length, 4}}} == expect(@params, "min_len_1", min_length: 4)
end

test "list: positive cases" do
list = [1, "a", :b]
assert {:ok, list} == expect(%{"list" => list}, "list", :list)
Expand Down
75 changes: 74 additions & 1 deletion apps/omg_watcher_info/lib/omg_watcher_info/api/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ defmodule OMG.WatcherInfo.API.Transaction do
def create(order) do
owner_inputs =
order.owner
|> DB.TxOutput.get_sorted_grouped_utxos()
|> DB.TxOutput.get_sorted_grouped_utxos(:desc)
|> TransactionCreator.select_inputs(order)

case owner_inputs do
Expand All @@ -106,6 +106,42 @@ defmodule OMG.WatcherInfo.API.Transaction do
end
end

@doc """
Converts parameter keyword list to a map before passing it to multi-clause "handle_merge/`1"
"""
@spec merge(Keyword.t()) :: create_t()
def merge(parameters) do
parameters |> Map.new() |> handle_merge()
end

@spec handle_merge(map()) :: create_t()
defp handle_merge(%{address: address, currency: currency}) do
merge_inputs =
address
|> DB.TxOutput.get_sorted_grouped_utxos(:asc)
|> Map.get(currency, [])

case merge_inputs do
[] ->
{:error, :no_inputs_found}

[_single_input] ->
{:error, :single_input}

inputs ->
{:ok, TransactionCreator.generate_merge_transactions(inputs)}
end
end

defp handle_merge(%{utxo_positions: utxo_positions}) do
with {:ok, inputs} <- get_merge_inputs(utxo_positions),
:ok <- no_duplicates(inputs),
:ok <- single_owner(inputs),
:ok <- single_currency(inputs) do
{:ok, TransactionCreator.generate_merge_transactions(inputs)}
kalouo marked this conversation as resolved.
Show resolved Hide resolved
end
end

defp get_utxos_count(currencies) do
Enum.reduce(currencies, 0, fn {_, currency_inputs}, acc -> acc + length(currency_inputs) end)
end
Expand All @@ -132,6 +168,43 @@ defmodule OMG.WatcherInfo.API.Transaction do
|> respond(:complete)
end

@spec get_merge_inputs(list()) :: {:ok, list()} | {:error, atom()}
defp get_merge_inputs(utxo_positions) do
Enum.reduce_while(utxo_positions, {:ok, []}, fn encoded_position, {:ok, acc} ->
case encoded_position |> Utxo.Position.decode!() |> DB.TxOutput.get_by_position() do
kalouo marked this conversation as resolved.
Show resolved Hide resolved
nil -> {:halt, {:error, :position_not_found}}
input -> {:cont, {:ok, [input | acc]}}
end
end)
end

@spec no_duplicates(list()) :: :ok | {:error, :duplicate_input_positions}
defp no_duplicates(inputs) do
inputs
|> Enum.uniq()
|> length()
|> case do
n when n == length(inputs) -> :ok
_ -> {:error, :duplicate_input_positions}
end
end

@spec single_owner(list()) :: :ok | {:error, :multiple_input_owners}
defp single_owner(inputs) do
case inputs |> Enum.uniq_by(fn input -> input.owner end) |> length() do
1 -> :ok
_ -> {:error, :multiple_input_owners}
end
end

@spec single_currency(list()) :: :ok | {:error, :multiple_currencies}
defp single_currency(inputs) do
case inputs |> Enum.uniq_by(fn input -> input.currency end) |> length() do
1 -> :ok
_ -> {:error, :multiple_currencies}
end
end

defp respond({:ok, transactions}, result), do: {:ok, %{result: result, transactions: transactions}}
defp respond(error, _), do: error
end
8 changes: 5 additions & 3 deletions apps/omg_watcher_info/lib/omg_watcher_info/db/txoutput.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ defmodule OMG.WatcherInfo.DB.TxOutput do
sigs: binary()
}

@type order_t() :: :asc | :desc

@primary_key false
schema "txoutputs" do
field(:blknum, :integer, primary_key: true)
Expand Down Expand Up @@ -209,14 +211,14 @@ defmodule OMG.WatcherInfo.DB.TxOutput do
end)
end

@spec get_sorted_grouped_utxos(OMG.Crypto.address_t()) :: %{OMG.Crypto.address_t() => list(%__MODULE__{})}
def get_sorted_grouped_utxos(owner) do
@spec get_sorted_grouped_utxos(OMG.Crypto.address_t(), order_t()) :: %{OMG.Crypto.address_t() => list(%__MODULE__{})}
def get_sorted_grouped_utxos(owner, order) do
# TODO: use clever DB query to get following out of DB
owner
|> get_all_utxos()
|> Enum.group_by(fn utxo -> utxo.currency end)
|> Enum.map(fn {currency, utxos} ->
{currency, Enum.sort_by(utxos, fn utxo -> utxo.amount end, :desc)}
{currency, Enum.sort_by(utxos, fn utxo -> utxo.amount end, order)}
end)
|> Map.new()
end
Expand Down
8 changes: 8 additions & 0 deletions apps/omg_watcher_info/lib/omg_watcher_info/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ defmodule OMG.WatcherInfo.Transaction do
}

@type utxos_map_t() :: %{UtxoSelection.currency_t() => UtxoSelection.utxo_list_t()}

@type inputs_t() :: {:ok, utxos_map_t()} | {:error, {:insufficient_funds, list(map())}}

@doc """
Expand Down Expand Up @@ -154,6 +155,13 @@ defmodule OMG.WatcherInfo.Transaction do
}
end

def include_typed_data({:ok, txs}) do
{
:ok,
%{transactions: Enum.map(txs, fn tx -> Map.put_new(tx, :typed_data, add_type_specs(tx)) end)}
}
end

@spec generate_merge_transactions(UtxoSelection.utxo_list_t()) :: list(transaction_t())
def generate_merge_transactions(merge_inputs) do
merge_inputs
Expand Down
191 changes: 191 additions & 0 deletions apps/omg_watcher_info/test/omg_watcher_info/api/transaction_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Copyright 2019-2020 OmiseGO Pte Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule OMG.WatcherInfo.API.TransactionTest do
use ExUnitFixtures
use ExUnit.Case, async: false
use OMG.WatcherInfo.Fixtures

alias OMG.Utxo.Position
alias OMG.WatcherInfo.API.Transaction

import OMG.WatcherInfo.Factory

@alice <<1::160>>
@bob <<2::160>>
@currency_1 <<3::160>>
@currency_2 <<4::160>>

describe "merge/1 with address and currency parameters" do
@tag fixtures: [:phoenix_ecto_sandbox]
test "merge with address and currency forms multiple merge transactions if possible" do
insert_initial_utxo()

_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)

assert {:ok, [%{inputs: [_, _, _, _], outputs: [output_1]}, %{inputs: [_, _, _], outputs: [output_2]}]} =
Transaction.merge(%{address: @alice, currency: @currency_1})

assert output_1 === %{amount: 4, currency: @currency_1, owner: @alice}
assert output_2 === %{amount: 3, currency: @currency_1, owner: @alice}
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "fetches inputs for the given addresss only" do
insert_initial_utxo()

_ = insert(:txoutput, currency: @currency_1, owner: @bob, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @bob, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)

assert {:ok, [alice_merge]} = Transaction.merge(%{address: @alice, currency: @currency_1})
assert %{inputs: [_, _, _, _], outputs: [%{amount: 4, currency: @currency_1, owner: @alice}]} = alice_merge
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "fetches inputs for the given currency only" do
insert_initial_utxo()

_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_2, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_2, owner: @alice, amount: 1)

assert {:ok, [alice_merge]} = Transaction.merge(%{address: @alice, currency: @currency_2})
assert %{inputs: [_, _], outputs: [%{amount: 2, currency: @currency_2, owner: @alice}]} = alice_merge
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "returns one merge transaction if five UTXOs are available – prioritising the lowest value inputs" do
insert_initial_utxo()

_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 2)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 3)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 4)
_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 5)

assert {:ok, [merge_tx]} = Transaction.merge(%{address: @alice, currency: @currency_1})

assert %{
inputs: [%{amount: 1}, %{amount: 2}, %{amount: 3}, %{amount: 4}],
outputs: [%{amount: 10, currency: @currency_1, owner: @alice}]
} = merge_tx
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "merge with address and currency fails on single input" do
insert_initial_utxo()

_ = insert(:txoutput, currency: @currency_1, owner: @alice, amount: 1)
_ = insert(:txoutput, currency: @currency_2, owner: @alice, amount: 1)

assert Transaction.merge(%{address: @alice, currency: @currency_1}) ==
{:error, :single_input}
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "returns expected error when no inputs are found" do
assert Transaction.merge(%{address: @alice, currency: @currency_1}) == {:error, :no_inputs_found}
end
end

describe "merge/1 with utxo_positions parameter" do
@tag fixtures: [:phoenix_ecto_sandbox]
test "given valid `utxo_positions` parameters, forms a merge transaction correctly" do
insert_initial_utxo()

position_1 =
:txoutput |> insert(owner: @alice, currency: @currency_1, amount: 1) |> encoded_position_from_insert()

position_2 =
:txoutput |> insert(owner: @alice, currency: @currency_1, amount: 1) |> encoded_position_from_insert()

position_3 =
:txoutput |> insert(owner: @alice, currency: @currency_1, amount: 1) |> encoded_position_from_insert()

{:ok, [%{inputs: inputs, outputs: outputs}]} =
Transaction.merge([{:utxo_positions, [position_1, position_2, position_3]}])

assert length(inputs) == 3
assert [%{amount: 3, currency: @currency_1, owner: @alice}] = outputs
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "returns an error if any duplicate positions are in the list" do
insert_initial_utxo()

position_1 = :txoutput |> insert() |> encoded_position_from_insert()

assert Transaction.merge([{:utxo_positions, [position_1, position_1]}]) ==
{:error, :duplicate_input_positions}
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "returns expected error if any position is not found" do
insert_initial_utxo()

position_1 = :txoutput |> insert() |> encoded_position_from_insert()
position_2 = :txoutput |> insert() |> encoded_position_from_insert()
position_3 = :txoutput |> insert() |> encoded_position_from_insert()

empty_position = insert(:txoutput) |> Map.update!(:blknum, fn n -> n + 1 end) |> encoded_position_from_insert()

assert Transaction.merge([{:utxo_positions, [position_1, position_2, position_3, empty_position]}]) ==
{:error, :position_not_found}
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "returns an error if there is more than one owner for the given set of UTXO positions" do
insert_initial_utxo()

position_1 = :txoutput |> insert(owner: @alice, currency: @currency_1) |> encoded_position_from_insert()
position_2 = :txoutput |> insert(owner: @alice, currency: @currency_1) |> encoded_position_from_insert()
position_3 = :txoutput |> insert(owner: @bob, currency: @currency_1) |> encoded_position_from_insert()

assert Transaction.merge([{:utxo_positions, [position_1, position_2, position_3]}]) ==
{:error, :multiple_input_owners}
end

@tag fixtures: [:phoenix_ecto_sandbox]
test "returns an error if there is more than one currency for the given set of UTXO positions" do
insert_initial_utxo()

position_1 = :txoutput |> insert(owner: @alice, currency: @currency_1) |> encoded_position_from_insert()
position_2 = :txoutput |> insert(owner: @alice, currency: @currency_1) |> encoded_position_from_insert()
position_3 = :txoutput |> insert(owner: @alice, currency: @currency_2) |> encoded_position_from_insert()

assert Transaction.merge([{:utxo_positions, [position_1, position_2, position_3]}]) ==
{:error, :multiple_currencies}
end
end

defp encoded_position_from_insert(%{oindex: oindex, txindex: txindex, blknum: blknum}) do
Position.encode({:utxo_position, blknum, txindex, oindex})
end

# This is needed so that UTXOs inserted subsequently can have a proper (non-zero) position
defp insert_initial_utxo() do
insert(:txoutput)
end
end
Loading