diff --git a/README.md b/README.md index f8d89d721..e2cb69bfe 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Current implemented features: - P2P transfers and genesis pools allocation - Transaction explorer - Custom Binary protocol leveraging Binary Pattern Matching and BitVectors +- NFT creation and transfers ## Next features to appear very soon: - Sampling P2P view on the Beacon chain diff --git a/config/test.exs b/config/test.exs index 2fdb258ca..4bd6117aa 100755 --- a/config/test.exs +++ b/config/test.exs @@ -4,6 +4,7 @@ use Mix.Config config :logger, level: :warning config :uniris, Uniris.Account.MemTablesLoader, enabled: false +config :uniris, Uniris.Account.MemTables.NFTLedger, enabled: false config :uniris, Uniris.Account.MemTables.UCOLedger, enabled: false config :uniris, Uniris.BeaconChain.Subset, enabled: false diff --git a/lib/uniris.ex b/lib/uniris.ex index c600ab0a0..da53d4eb8 100644 --- a/lib/uniris.ex +++ b/lib/uniris.ex @@ -108,7 +108,7 @@ defmodule Uniris do If the current node is a storage of this address, it will perform a fast lookup Otherwise it will request the closest storage node about it """ - @spec get_balance(binary) :: uco_balance :: float() + @spec get_balance(binary) :: Accout.balance() def get_balance(address) when is_binary(address) do storage_nodes = address @@ -118,12 +118,12 @@ defmodule Uniris do if Utils.key_in_node_list?(storage_nodes, Crypto.node_public_key(0)) do Account.get_balance(address) else - %Balance{uco: uco_balance} = + %Balance{uco: uco_balance, nft: nft_balances} = storage_nodes |> P2P.broadcast_message(%GetBalance{address: address}) |> Enum.at(0) - uco_balance + %{uco: uco_balance, nft: nft_balances} end end diff --git a/lib/uniris/account.ex b/lib/uniris/account.ex index 3ef5cf7a5..10e39a563 100644 --- a/lib/uniris/account.ex +++ b/lib/uniris/account.ex @@ -1,30 +1,48 @@ defmodule Uniris.Account do @moduledoc false + alias __MODULE__.MemTables.NFTLedger alias __MODULE__.MemTables.UCOLedger alias __MODULE__.MemTablesLoader + alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput + + @type balance :: %{ + uco: amount :: float(), + nft: %{(address :: binary()) => amount :: float()} + } + @doc """ Returns the balance for an address using the unspent outputs """ - @spec get_balance(Crypto.versioned_hash()) :: float() + @spec get_balance(Crypto.versioned_hash()) :: balance() def get_balance(address) when is_binary(address) do address - |> UCOLedger.get_unspent_outputs() - |> Enum.reduce(0.0, &(&2 + &1.amount)) + |> get_unspent_outputs() + |> Enum.reduce(%{uco: 0.0, nft: %{}}, fn + %UnspentOutput{type: :UCO, amount: amount}, acc -> + Map.update!(acc, :uco, &(&1 + amount)) + + %UnspentOutput{type: {:NFT, nft_address}, amount: amount}, acc -> + update_in(acc, [:nft, Access.key(nft_address, 0.0)], &(&1 + amount)) + end) end @doc """ List all the unspent outputs for a given address """ @spec get_unspent_outputs(binary()) :: list(UnspentOutput.t()) - defdelegate get_unspent_outputs(address), to: UCOLedger + def get_unspent_outputs(address) do + UCOLedger.get_unspent_outputs(address) ++ NFTLedger.get_unspent_outputs(address) + end @doc """ List all the inputs for a given transaction (including the spend/unspent inputs) """ @spec get_inputs(binary()) :: list(TransactionInput.t()) - defdelegate get_inputs(address), to: UCOLedger, as: :get_inputs + def get_inputs(address) do + UCOLedger.get_inputs(address) ++ NFTLedger.get_inputs(address) + end @doc """ Load the transaction into the Account context filling the memory tables for ledgers diff --git a/lib/uniris/account/mem_tables/nft_ledger.ex b/lib/uniris/account/mem_tables/nft_ledger.ex new file mode 100644 index 000000000..0adf4b3cb --- /dev/null +++ b/lib/uniris/account/mem_tables/nft_ledger.ex @@ -0,0 +1,185 @@ +defmodule Uniris.Account.MemTables.NFTLedger do + @moduledoc false + + @ledger_table :uniris_nft_ledger + @unspent_output_index_table :uniris_nft_unspent_output_index + + alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput + alias Uniris.TransactionChain.TransactionInput + + use GenServer + + require Logger + + @doc """ + Initialize the NFT ledger tables: + - Main NFT ledger as ETS set ({nft, to, from}, amount, spent?) + - NFT Unspent Output Index as ETS bag (to, {from, nft}) + + ## Examples + + iex> {:ok, _} = NFTLedger.start_link() + iex> { :ets.info(:uniris_nft_ledger)[:type], :ets.info(:uniris_nft_unspent_output_index)[:type] } + { :set, :bag } + """ + def start_link(args \\ []) do + GenServer.start_link(__MODULE__, args) + end + + def init(_) do + Logger.info("Initialize InMemory UCO Ledger...") + + :ets.new(@ledger_table, [:set, :named_table, :public, read_concurrency: true]) + + :ets.new(@unspent_output_index_table, [ + :bag, + :named_table, + :public, + read_concurrency: true + ]) + + {:ok, + %{ + ledger_table: @ledger_table, + unspent_outputs_index_table: @unspent_output_index_table + }} + end + + @doc """ + Add an unspent output to the ledger for the recipient address + + ## Examples + + iex> {:ok, pid} = NFTLedger.start_link() + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: {:NFT, "@NFT1"}}) + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: {:NFT, "@NFT1"}}) + iex> { :ets.tab2list(:uniris_nft_ledger), :ets.tab2list(:uniris_nft_unspent_output_index) } + { + [ + {{"@Alice2", "@Bob3", "@NFT1"}, 3.0, false}, + {{"@Alice2", "@Charlie10", "@NFT1"}, 1.0, false} + ], + [ + {"@Alice2", "@Bob3", "@NFT1"}, + {"@Alice2", "@Charlie10", "@NFT1"} + ] + } + + """ + @spec add_unspent_output(binary(), UnspentOutput.t()) :: :ok + def add_unspent_output(to_address, %UnspentOutput{ + from: from_address, + amount: amount, + type: {:NFT, nft_address} + }) + when is_binary(to_address) and is_binary(from_address) and is_float(amount) and + is_binary(nft_address) do + true = :ets.insert(@ledger_table, {{to_address, from_address, nft_address}, amount, false}) + true = :ets.insert(@unspent_output_index_table, {to_address, from_address, nft_address}) + :ok + end + + @doc """ + Get the unspent outputs for a given transaction address + + ## Examples + + iex> {:ok, pid} = NFTLedger.start_link() + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: {:NFT, "@NFT1"}}) + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: {:NFT, "@NFT1"}}) + iex> NFTLedger.get_unspent_outputs("@Alice2") + [ + %UnspentOutput{from: "@Charlie10", amount: 1.0, type: {:NFT, "@NFT1"}}, + %UnspentOutput{from: "@Bob3", amount: 3.0, type: {:NFT, "@NFT1"}}, + ] + + iex> {:ok, pid} = NFTLedger.start_link() + iex> NFTLedger.get_unspent_outputs("@Alice2") + [] + """ + @spec get_unspent_outputs(binary()) :: list(UnspentOutput.t()) + def get_unspent_outputs(address) when is_binary(address) do + @unspent_output_index_table + |> :ets.lookup(address) + |> Enum.reduce([], fn {_, from, nft_address}, acc -> + case :ets.lookup(@ledger_table, {address, from, nft_address}) do + [{_, amount, false}] -> + [ + %UnspentOutput{ + from: from, + amount: amount, + type: {:NFT, nft_address} + } + | acc + ] + + _ -> + acc + end + end) + end + + @doc """ + Spend all the unspent outputs for the given address + + ## Examples + + iex> {:ok, pid} = NFTLedger.start_link() + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: {:NFT, "@NFT1"}}) + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: {:NFT, "@NFT1"}}) + iex> :ok = NFTLedger.spend_all_unspent_outputs("@Alice2") + iex> NFTLedger.get_unspent_outputs("@Alice2") + [] + + """ + @spec spend_all_unspent_outputs(binary()) :: :ok + def spend_all_unspent_outputs(address) do + @unspent_output_index_table + |> :ets.lookup(address) + |> Enum.each(fn {_, from, nft_address} -> + :ets.update_element(@ledger_table, {address, from, nft_address}, {3, true}) + end) + + :ok + end + + @doc """ + Retrieve the entire inputs for a given address (spent or unspent) + + ## Examples + + iex> {:ok, pid} = NFTLedger.start_link() + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: {:NFT, "@NFT1"}}) + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: {:NFT, "@NFT1"}}) + iex> NFTLedger.get_inputs("@Alice2") + [ + %TransactionInput{from: "@Bob3", amount: 3.0, type: {:NFT, "@NFT1"}, spent?: false}, + %TransactionInput{from: "@Charlie10", amount: 1.0, type: {:NFT, "@NFT1"}, spent?: false} + ] + + iex> {:ok, pid} = NFTLedger.start_link() + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: {:NFT, "@NFT1"}}) + iex> :ok = NFTLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: {:NFT, "@NFT1"}}) + iex> :ok = NFTLedger.spend_all_unspent_outputs("@Alice2") + iex> NFTLedger.get_inputs("@Alice2") + [ + %TransactionInput{from: "@Bob3", amount: 3.0, type: {:NFT, "@NFT1"}, spent?: true}, + %TransactionInput{from: "@Charlie10", amount: 1.0, type: {:NFT, "@NFT1"}, spent?: true} + ] + """ + @spec get_inputs(binary()) :: list(TransactionInput.t()) + def get_inputs(address) when is_binary(address) do + @unspent_output_index_table + |> :ets.lookup(address) + |> Enum.map(fn {_, from, nft_address} -> + [{_, amount, spent?}] = :ets.lookup(@ledger_table, {address, from, nft_address}) + + %TransactionInput{ + from: from, + amount: amount, + type: {:NFT, nft_address}, + spent?: spent? + } + end) + end +end diff --git a/lib/uniris/account/mem_tables/uco_ledger.ex b/lib/uniris/account/mem_tables/uco_ledger.ex index 93305f55b..c2cd3ba40 100644 --- a/lib/uniris/account/mem_tables/uco_ledger.ex +++ b/lib/uniris/account/mem_tables/uco_ledger.ex @@ -51,8 +51,8 @@ defmodule Uniris.Account.MemTables.UCOLedger do ## Examples iex> {:ok, pid} = UCOLedger.start_link() - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0}) - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: :UCO}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: :UCO}) iex> { :ets.tab2list(:uniris_uco_ledger), :ets.tab2list(:uniris_uco_unspent_output_index) } { [ @@ -79,12 +79,12 @@ defmodule Uniris.Account.MemTables.UCOLedger do ## Examples iex> {:ok, pid} = UCOLedger.start_link() - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0}) - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: :UCO}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: :UCO}) iex> UCOLedger.get_unspent_outputs("@Alice2") [ - %UnspentOutput{from: "@Charlie10", amount: 1.0}, - %UnspentOutput{from: "@Bob3", amount: 3.0}, + %UnspentOutput{from: "@Charlie10", amount: 1.0, type: :UCO}, + %UnspentOutput{from: "@Bob3", amount: 3.0, type: :UCO}, ] iex> {:ok, pid} = UCOLedger.start_link() @@ -101,7 +101,8 @@ defmodule Uniris.Account.MemTables.UCOLedger do [ %UnspentOutput{ from: from, - amount: amount + amount: amount, + type: :UCO } | acc ] @@ -118,8 +119,8 @@ defmodule Uniris.Account.MemTables.UCOLedger do ## Examples iex> {:ok, pid} = UCOLedger.start_link() - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0}) - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: :UCO}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: :UCO}) iex> :ok = UCOLedger.spend_all_unspent_outputs("@Alice2") iex> UCOLedger.get_unspent_outputs("@Alice2") [] @@ -140,22 +141,22 @@ defmodule Uniris.Account.MemTables.UCOLedger do ## Examples iex> {:ok, pid} = UCOLedger.start_link() - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0}) - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: :UCO}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: :UCO}) iex> UCOLedger.get_inputs("@Alice2") [ - %TransactionInput{from: "@Bob3", amount: 3.0, spent?: false}, - %TransactionInput{from: "@Charlie10", amount: 1.0, spent?: false} + %TransactionInput{from: "@Bob3", amount: 3.0, spent?: false, type: :UCO}, + %TransactionInput{from: "@Charlie10", amount: 1.0, spent?: false, type: :UCO} ] iex> {:ok, pid} = UCOLedger.start_link() - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0}) - iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Bob3", amount: 3.0, type: :UCO}) + iex> :ok = UCOLedger.add_unspent_output("@Alice2", %UnspentOutput{from: "@Charlie10", amount: 1.0, type: :UCO}) iex> :ok = UCOLedger.spend_all_unspent_outputs("@Alice2") iex> UCOLedger.get_inputs("@Alice2") [ - %TransactionInput{from: "@Bob3", amount: 3.0, spent?: true}, - %TransactionInput{from: "@Charlie10", amount: 1.0, spent?: true} + %TransactionInput{from: "@Bob3", amount: 3.0, spent?: true, type: :UCO}, + %TransactionInput{from: "@Charlie10", amount: 1.0, spent?: true, type: :UCO} ] """ @spec get_inputs(binary()) :: list(TransactionInput.t()) @@ -168,7 +169,8 @@ defmodule Uniris.Account.MemTables.UCOLedger do %TransactionInput{ from: from, amount: amount, - spent?: spent? + spent?: spent?, + type: :UCO } end) end diff --git a/lib/uniris/account/mem_tables_loader.ex b/lib/uniris/account/mem_tables_loader.ex index b203aaa1e..4eb753494 100644 --- a/lib/uniris/account/mem_tables_loader.ex +++ b/lib/uniris/account/mem_tables_loader.ex @@ -3,6 +3,7 @@ defmodule Uniris.Account.MemTablesLoader do use GenServer + alias Uniris.Account.MemTables.NFTLedger alias Uniris.Account.MemTables.UCOLedger alias Uniris.Bootstrap @@ -13,6 +14,7 @@ defmodule Uniris.Account.MemTablesLoader do alias Uniris.TransactionChain.Transaction alias Uniris.TransactionChain.Transaction.ValidationStamp alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations + alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.TransactionMovement alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput require Logger @@ -62,9 +64,10 @@ defmodule Uniris.Account.MemTablesLoader do } } }) do - previous_public_key - |> Crypto.hash() - |> UCOLedger.spend_all_unspent_outputs() + previous_address = Crypto.hash(previous_public_key) + + UCOLedger.spend_all_unspent_outputs(previous_address) + NFTLedger.spend_all_unspent_outputs(previous_address) :ok = set_transaction_movements(address, transaction_movements) :ok = set_unspent_outputs(address, unspent_outputs) @@ -76,14 +79,27 @@ defmodule Uniris.Account.MemTablesLoader do end defp set_transaction_movements(address, transaction_movements) do - Enum.each( - transaction_movements, - &UCOLedger.add_unspent_output(&1.to, %UnspentOutput{amount: &1.amount, from: address}) - ) + Enum.each(transaction_movements, fn + %TransactionMovement{to: to, amount: amount, type: :UCO} -> + UCOLedger.add_unspent_output(to, %UnspentOutput{amount: amount, from: address, type: :UCO}) + + %TransactionMovement{to: to, amount: amount, type: {:NFT, nft_address}} -> + NFTLedger.add_unspent_output(to, %UnspentOutput{ + amount: amount, + from: address, + type: {:NFT, nft_address} + }) + end) end defp set_unspent_outputs(address, unspent_outputs) do - Enum.each(unspent_outputs, &UCOLedger.add_unspent_output(address, &1)) + Enum.each(unspent_outputs, fn + unspent_output = %UnspentOutput{type: :UCO} -> + UCOLedger.add_unspent_output(address, unspent_output) + + unspent_output = %UnspentOutput{type: {:NFT, _nft_address}} -> + NFTLedger.add_unspent_output(address, unspent_output) + end) end defp set_node_rewards(address, node_movements) do @@ -91,7 +107,8 @@ defmodule Uniris.Account.MemTablesLoader do node_movements, &UCOLedger.add_unspent_output(Crypto.hash(&1.to), %UnspentOutput{ amount: &1.amount, - from: address + from: address, + type: :UCO }) ) end diff --git a/lib/uniris/account/supervisor.ex b/lib/uniris/account/supervisor.ex index 5dbc624ea..67bad8166 100644 --- a/lib/uniris/account/supervisor.ex +++ b/lib/uniris/account/supervisor.ex @@ -3,6 +3,7 @@ defmodule Uniris.Account.Supervisor do use Supervisor + alias Uniris.Account.MemTables.NFTLedger alias Uniris.Account.MemTables.UCOLedger alias Uniris.Account.MemTablesLoader @@ -14,6 +15,7 @@ defmodule Uniris.Account.Supervisor do def init(_args) do children = [ + NFTLedger, UCOLedger, MemTablesLoader ] diff --git a/lib/uniris/bootstrap/network_init.ex b/lib/uniris/bootstrap/network_init.ex index ed581d2d7..ee1f1c73b 100644 --- a/lib/uniris/bootstrap/network_init.ex +++ b/lib/uniris/bootstrap/network_init.ex @@ -29,8 +29,8 @@ defmodule Uniris.Bootstrap.NetworkInit do alias Uniris.TransactionChain.TransactionData alias Uniris.TransactionChain.TransactionData.Ledger - alias Uniris.TransactionChain.TransactionData.Ledger.Transfer alias Uniris.TransactionChain.TransactionData.UCOLedger + alias Uniris.TransactionChain.TransactionData.UCOLedger.Transfer require Logger @@ -101,15 +101,14 @@ defmodule Uniris.Bootstrap.NetworkInit do genesis_transfers_amount = tx |> Transaction.get_movements() - |> Enum.reduce(0.0, &(&2 + &1.amount)) - - unspent_output_amount = genesis_transfers_amount + Transaction.fee(tx) + |> Enum.reduce(Transaction.fee(tx), &(&2 + &1.amount)) tx |> self_validation!([ %UnspentOutput{ from: Bootstrap.genesis_unspent_output_address(), - amount: unspent_output_amount + amount: genesis_transfers_amount, + type: :UCO } ]) |> self_replication() diff --git a/lib/uniris/crypto.ex b/lib/uniris/crypto.ex index de637c43e..8666b6a2e 100755 --- a/lib/uniris/crypto.ex +++ b/lib/uniris/crypto.ex @@ -278,10 +278,10 @@ defmodule Uniris.Crypto do iex> {pub, _} = Crypto.generate_deterministic_keypair("myseed", :secp256r1) iex> pub - <<1, 4, 71, 234, 56, 77, 247, 36, 202, 205, 0, 115, 85, 40, 74, 90, 107, 180, - 162, 184, 168, 248, 179, 160, 69, 68, 159, 128, 0, 23, 81, 29, 122, 89, 51, - 182, 115, 31, 213, 158, 244, 116, 92, 197, 246, 196, 55, 27, 8, 205, 62, 39, - 55, 227, 59, 94, 246, 213, 26, 22, 150, 137, 167, 23, 69, 144>> + <<1, 4, 140, 235, 188, 198, 146, 160, 92, 132, 81, 177, 113, 230, 39, 220, 122, + 112, 231, 18, 90, 66, 156, 47, 54, 192, 141, 44, 45, 223, 115, 28, 30, 48, + 105, 253, 171, 105, 87, 148, 108, 150, 86, 128, 28, 102, 163, 51, 28, 57, 33, + 133, 109, 49, 202, 92, 184, 138, 187, 26, 123, 45, 5, 94, 180, 250>> """ @spec generate_deterministic_keypair( diff --git a/lib/uniris/crypto/ecdsa.ex b/lib/uniris/crypto/ecdsa.ex index 9ff8ea4c8..e866cfec3 100755 --- a/lib/uniris/crypto/ecdsa.ex +++ b/lib/uniris/crypto/ecdsa.ex @@ -9,11 +9,20 @@ defmodule Uniris.Crypto.ECDSA do Generate an ECDSA keypair from a given secrets """ @spec generate_keypair(curve(), binary()) :: {binary(), binary()} + def generate_keypair(curve, seed) + when curve in @curves and is_binary(seed) and byte_size(seed) < 32 do + :crypto.generate_key( + :ecdh, + curve, + :crypto.hash(:sha256, seed) + ) + end + def generate_keypair(curve, seed) when curve in @curves and is_binary(seed) do :crypto.generate_key( :ecdh, curve, - seed + :binary.part(seed, 0, 32) ) end diff --git a/lib/uniris/mining.ex b/lib/uniris/mining.ex index 4655b37f1..2b45edf42 100644 --- a/lib/uniris/mining.ex +++ b/lib/uniris/mining.ex @@ -157,6 +157,10 @@ defmodule Uniris.Mining do end end + defp do_accept_transaction?(%Transaction{type: :nft, data: %TransactionData{content: content}}) do + Regex.match?(~r/(?<=initial supply:).*\d/mi, content) + end + defp do_accept_transaction?(_), do: true defp get_first_public_key(tx = %Transaction{previous_public_key: previous_public_key}) do diff --git a/lib/uniris/mining/proof_of_work.ex b/lib/uniris/mining/proof_of_work.ex index b2db89924..888d1c699 100644 --- a/lib/uniris/mining/proof_of_work.ex +++ b/lib/uniris/mining/proof_of_work.ex @@ -39,8 +39,8 @@ defmodule Uniris.Mining.ProofOfWork do ...> 223, 189, 95, 253, 38, 40, 243, 54, 27, 96, 70, 86, 220, 217, 9, 1>>, ...> <<0, 211, 80, 49, 147, 126, 126, 253, 230, 87, 77, 68, 164, 77, 212, 75, 123, ...> 37, 92, 251, 236, 251, 102, 255, 147, 203, 168, 147, 192, 65, 28, 13, 13>>, - ...> <<0, 31, 2, 133, 98, 148, 6, 94, 233, 149, 174, 109, 92, 220, 4, 28, 164, 227, - ...> 123, 164, 104, 42, 110, 86, 189, 190, 156, 60, 142, 2, 44, 5, 44>>, + ...> <<0, 156, 198, 40, 89, 184, 32, 101, 103, 168, 90, 234, 89, 93, 170, 89, 45, + ...> 100, 237, 251, 223, 10, 130, 88, 124, 15, 21, 74, 28, 33, 245, 142, 179>>, ...> <<0, 25, 36, 103, 151, 183, 40, 176, 220, 225, 176, 57, 61, 203, 57, 118, 134, ...> 150, 41, 194, 35, 35, 160, 145, 98, 31, 36, 154, 209, 151, 12, 125, 142>> ...> ] @@ -49,11 +49,10 @@ defmodule Uniris.Mining.ProofOfWork do ...> 71, 189, 178, 226, 124, 57, 18, 0, 115, 106, 182, 71, 149, 191, 76, 168, ...> 248, 14, 164>>, ...> data: %TransactionData{}, - ...> origin_signature: <<176, 63, 162, 229, 68, 196, 200, 91, 107, 82, 34, 52, 227, - ...> 51, 87, 243, 146, 104, 174, 221, 249, 42, 228, 240, 243, 34, 107, 251, 39, - ...> 188, 52, 120, 97, 201, 225, 105, 232, 100, 2, 203, 85, 71, 184, 17, 172, - ...> 102, 124, 175, 1, 114, 143, 235, 11, 21, 186, 12, 144, 103, 82, 33, 118, - ...> 200, 74, 4>>, + ...> origin_signature: <<26, 190, 16, 11, 199, 13, 254, 146, 143, 211, 235, 233, 105, 162, 135, 200, + ...> 64, 219, 119, 228, 253, 176, 183, 241, 98, 13, 161, 117, 41, 252, 44, 203, + ...> 126, 44, 22, 254, 55, 253, 6, 112, 222, 132, 144, 63, 57, 135, 122, 252, 145, + ...> 50, 134, 68, 207, 58, 32, 34, 85, 154, 221, 133, 137, 250, 54, 11>>, ...> previous_public_key: <<0, 110, 226, 20, 197, 55, 224, 165, 95, 201, 111, 210, ...> 50, 138, 25, 142, 130, 140, 51, 143, 208, 228, 230, 150, 84, 161, 157, 32, ...> 42, 55, 118, 226, 12>>, @@ -68,8 +67,8 @@ defmodule Uniris.Mining.ProofOfWork do { :ok, # The 4th public key matches - <<0, 31, 2, 133, 98, 148, 6, 94, 233, 149, 174, 109, 92, 220, 4, 28, 164, 227, - 123, 164, 104, 42, 110, 86, 189, 190, 156, 60, 142, 2, 44, 5, 44>> + <<0, 156, 198, 40, 89, 184, 32, 101, 103, 168, 90, 234, 89, 93, 170, 89, 45, + 100, 237, 251, 223, 10, 130, 88, 124, 15, 21, 74, 28, 33, 245, 142, 179>> } """ @spec find_transaction_origin_public_key(list(Crypto.key()), Transaction.t()) :: diff --git a/lib/uniris/mining/transaction_context/data_fetcher.ex b/lib/uniris/mining/transaction_context/data_fetcher.ex index 2e5398d3a..952518bf5 100644 --- a/lib/uniris/mining/transaction_context/data_fetcher.ex +++ b/lib/uniris/mining/transaction_context/data_fetcher.ex @@ -98,35 +98,48 @@ defmodule Uniris.Mining.TransactionContext.DataFetcher do end) end - defp confirm_unspent_output( - unspent_output = %UnspentOutput{from: from, amount: amount}, - tx_address - ) do - response = + defp confirm_unspent_output(unspent_output = %UnspentOutput{from: from}, tx_address) do + {res, node} = from |> Replication.chain_storage_nodes(P2P.list_nodes(availability: :global)) |> P2P.broadcast_message(%GetTransaction{address: from}, ack_node?: true, timeout: 500) |> Enum.at(0) - case response do - {tx = %Transaction{}, node} -> - %Transaction{ - validation_stamp: %ValidationStamp{ - ledger_operations: %LedgerOperations{transaction_movements: tx_movements} - } - } = tx - - if Enum.any?(tx_movements, &(&1.to == tx_address && &1.amount == amount)) do - {:ok, unspent_output, node} - else - {:error, :invalid_unspent_output} - end - - _ -> - {:error, :invalid_unspent_output} + if valid_unspent_output?(tx_address, unspent_output, res) do + {:ok, unspent_output, node} + else + {:error, :invalid_unspent_output} end end + defp valid_unspent_output?(tx_address, %UnspentOutput{amount: amount, type: type}, %Transaction{ + validation_stamp: %ValidationStamp{ + ledger_operations: %LedgerOperations{ + transaction_movements: tx_movements, + unspent_outputs: unspent_outputs + } + } + }) do + cond do + Enum.any?( + tx_movements, + &(&1.to == tx_address and &1.amount == amount and &1.type == type) + ) -> + true + + Enum.any?( + unspent_outputs, + &(&1.from == tx_address and &1.type == type and &1.amount == amount) + ) -> + true + + true -> + false + end + end + + defp valid_unspent_output?(_, _, _), do: false + @doc """ Request to a set a storage nodes the P2P view of some nodes diff --git a/lib/uniris/p2p/message.ex b/lib/uniris/p2p/message.ex index 0da382295..bcbedafb6 100644 --- a/lib/uniris/p2p/message.ex +++ b/lib/uniris/p2p/message.ex @@ -275,8 +275,16 @@ defmodule Uniris.P2P.Message do <<247::8, digest::binary>> end - def encode(%Balance{uco: uco_balance}) do - <<248::8, uco_balance::float>> + def encode(%Balance{uco: uco_balance, nft: nft_balances}) do + nft_balances_binary = + nft_balances + |> Enum.reduce([], fn {nft_address, amount}, acc -> + [<> | acc] + end) + |> Enum.reverse() + |> :erlang.list_to_binary() + + <<248::8, uco_balance::float, map_size(nft_balances)::16, nft_balances_binary::binary>> end def encode(%BeaconSlotList{slots: slots}) do @@ -571,9 +579,12 @@ defmodule Uniris.P2P.Message do } end - def decode(<<248::8, uco_balance::float>>) do + def decode(<<248::8, uco_balance::float, nb_nft_balances::16, rest::bitstring>>) do + {nft_balances, _} = deserialize_nft_balances(rest, nb_nft_balances, %{}) + %Balance{ - uco: uco_balance + uco: uco_balance, + nft: nft_balances } end @@ -702,6 +713,17 @@ defmodule Uniris.P2P.Message do deserialize_transaction_inputs(rest, nb_inputs, [input | acc]) end + defp deserialize_nft_balances(rest, 0, _acc), do: {%{}, rest} + + defp deserialize_nft_balances(rest, nft_balances, acc) when map_size(acc) == nft_balances do + {acc, rest} + end + + defp deserialize_nft_balances(rest, nb_nft_balances, acc) do + {nft_address, <>} = deserialize_hash(rest) + deserialize_nft_balances(rest, nb_nft_balances, Map.put(acc, nft_address, amount)) + end + # TODO: support streaming @doc """ Handle a P2P message by processing it through the dedicated context diff --git a/lib/uniris/p2p/message/balance.ex b/lib/uniris/p2p/message/balance.ex index 9d5611406..89bd44632 100644 --- a/lib/uniris/p2p/message/balance.ex +++ b/lib/uniris/p2p/message/balance.ex @@ -2,9 +2,10 @@ defmodule Uniris.P2P.Message.Balance do @moduledoc """ Represents a message with the balance of a transaction """ - defstruct uco: 0.0 + defstruct uco: 0.0, nft: %{} @type t :: %__MODULE__{ - uco: float() + uco: float(), + nft: %{binary() => float()} } end diff --git a/lib/uniris/release_task.ex b/lib/uniris/release_task.ex index a6a195e28..84d0d729b 100644 --- a/lib/uniris/release_task.ex +++ b/lib/uniris/release_task.ex @@ -9,8 +9,8 @@ defmodule Uniris.ReleaseTask do alias Uniris.TransactionChain.Transaction alias Uniris.TransactionChain.TransactionData alias Uniris.TransactionChain.TransactionData.Ledger - alias Uniris.TransactionChain.TransactionData.Ledger.Transfer alias Uniris.TransactionChain.TransactionData.UCOLedger + alias Uniris.TransactionChain.TransactionData.UCOLedger.Transfer # TODO: to remove once the Client UI developed def transfer_to_website_addresses(index \\ 0, destination_index \\ 0, amount \\ 1.0) do diff --git a/lib/uniris/replication/transaction_context.ex b/lib/uniris/replication/transaction_context.ex index b7ec8e4a1..edb3ad077 100644 --- a/lib/uniris/replication/transaction_context.ex +++ b/lib/uniris/replication/transaction_context.ex @@ -40,10 +40,10 @@ defmodule Uniris.Replication.TransactionContext do |> Replication.chain_storage_nodes(P2P.list_nodes(availability: :global)) |> P2P.nearest_nodes() |> P2P.broadcast_message(%GetUnspentOutputs{address: address}) - |> Stream.flat_map(fn %UnspentOutputList{unspent_outputs: unspent_outputs} -> + |> Stream.take(1) + |> Enum.flat_map(fn %UnspentOutputList{unspent_outputs: unspent_outputs} -> unspent_outputs end) - |> Stream.take(1) end @spec fetch_transaction_inputs(binary()) :: list(TransactionInput.t()) diff --git a/lib/uniris/transaction_chain.ex b/lib/uniris/transaction_chain.ex index ca7f71439..37d3e1649 100644 --- a/lib/uniris/transaction_chain.ex +++ b/lib/uniris/transaction_chain.ex @@ -246,8 +246,8 @@ defmodule Uniris.TransactionChain do ...> ] ...> |> TransactionChain.proof_of_integrity() # Hash of the transaction - <<0, 51, 88, 180, 249, 223, 216, 111, 207, 144, 158, 83, 19, 231, 40, 11, 52, - 46, 113, 125, 121, 46, 182, 250, 186, 45, 72, 126, 229, 220, 111, 221, 240>> + <<0, 50, 95, 59, 58, 51, 188, 130, 165, 215, 204, 134, 20, 115, 49, 23, 57, 225, + 100, 254, 239, 53, 53, 252, 79, 70, 0, 89, 179, 108, 175, 223, 210>> With multiple transactions @@ -294,15 +294,15 @@ defmodule Uniris.TransactionChain do ...> 161, 155, 143, 43, 50, 6, 7, 97, 130, 134, 174, 7, 235, 183, 88, 165, 197, 25, 219, 84, ...> 232, 135, 42, 112, 58, 181, 13>>, ...> validation_stamp: %ValidationStamp{ - ...> proof_of_integrity: <<0, 51, 88, 180, 249, 223, 216, 111, 207, 144, 158, 83, 19, 231, 40, 11, 52, - ...> 46, 113, 125, 121, 46, 182, 250, 186, 45, 72, 126, 229, 220, 111, 221, 240>> + ...> proof_of_integrity: <<0, 50, 95, 59, 58, 51, 188, 130, 165, 215, 204, 134, 20, 115, 49, 23, 57, 225, + ...> 100, 254, 239, 53, 53, 252, 79, 70, 0, 89, 179, 108, 175, 223, 210>> ...> } ...> } ...> ] ...> |> TransactionChain.proof_of_integrity() # Hash of the transaction + previous proof of integrity - <<0, 155, 254, 30, 19, 107, 70, 29, 112, 74, 169, 137, 198, 116, 67, 134, 7, 46, - 251, 124, 214, 64, 45, 40, 104, 21, 58, 133, 133, 5, 133, 118, 28>> + <<0, 200, 7, 18, 182, 180, 36, 204, 30, 173, 77, 225, 44, 8, 160, 137, 240, 109, + 39, 97, 151, 130, 32, 22, 218, 28, 66, 121, 188, 106, 156, 61, 181>> """ @spec proof_of_integrity(nonempty_list(Transaction.t())) :: binary() def proof_of_integrity([ @@ -349,9 +349,9 @@ defmodule Uniris.TransactionChain do ...> 161, 155, 143, 43, 50, 6, 7, 97, 130, 134, 174, 7, 235, 183, 88, 165, 197, 25, 219, 84, ...> 232, 135, 42, 112, 58, 181, 13>>, ...> validation_stamp: %ValidationStamp{ - ...> proof_of_integrity: <<0, 155, 254, 30, 19, 107, 70, 29, 112, 74, 169, 137, 198, 116, 67, 134, 7, 46, - ...> 251, 124, 214, 64, 45, 40, 104, 21, 58, 133, 133, 5, 133, 118, 28>> - ...> } + ...> proof_of_integrity: <<0, 200, 7, 18, 182, 180, 36, 204, 30, 173, 77, 225, 44, 8, 160, 137, 240, 109, + ...> 39, 97, 151, 130, 32, 22, 218, 28, 66, 121, 188, 106, 156, 61, 181>> + ...> } ...> }, ...> %Transaction{ ...> address: @@ -374,8 +374,8 @@ defmodule Uniris.TransactionChain do ...> 161, 155, 143, 43, 50, 6, 7, 97, 130, 134, 174, 7, 235, 183, 88, 165, 197, 25, 219, 84, ...> 232, 135, 42, 112, 58, 181, 13>>, ...> validation_stamp: %ValidationStamp{ - ...> proof_of_integrity: <<0, 51, 88, 180, 249, 223, 216, 111, 207, 144, 158, 83, 19, 231, 40, 11, 52, - ...> 46, 113, 125, 121, 46, 182, 250, 186, 45, 72, 126, 229, 220, 111, 221, 240>> + ...> proof_of_integrity: <<0, 50, 95, 59, 58, 51, 188, 130, 165, 215, 204, 134, 20, 115, 49, 23, 57, 225, + ...> 100, 254, 239, 53, 53, 252, 79, 70, 0, 89, 179, 108, 175, 223, 210>> ...> } ...> } ...> ] diff --git a/lib/uniris/transaction_chain/transaction.ex b/lib/uniris/transaction_chain/transaction.ex index f4a4f1b73..a7f1dad55 100755 --- a/lib/uniris/transaction_chain/transaction.ex +++ b/lib/uniris/transaction_chain/transaction.ex @@ -14,6 +14,7 @@ defmodule Uniris.TransactionChain.Transaction do alias Uniris.TransactionChain.TransactionData alias Uniris.TransactionChain.TransactionData.Ledger + alias Uniris.TransactionChain.TransactionData.NFTLedger alias Uniris.TransactionChain.TransactionData.UCOLedger defstruct [ @@ -68,6 +69,7 @@ defmodule Uniris.TransactionChain.Transaction do | :hosting | :code_proposal | :code_approval + | :nft @transaction_types [ :identity, @@ -79,7 +81,8 @@ defmodule Uniris.TransactionChain.Transaction do :beacon, :hosting, :code_proposal, - :code_approval + :code_approval, + :nft ] @doc """ @@ -260,6 +263,7 @@ defmodule Uniris.TransactionChain.Transaction do def serialize_type(:hosting), do: 7 def serialize_type(:code_proposal), do: 8 def serialize_type(:code_approval), do: 9 + def serialize_type(:nft), do: 10 @doc """ Parse a serialize transaction type @@ -275,6 +279,7 @@ defmodule Uniris.TransactionChain.Transaction do def parse_type(7), do: :hosting def parse_type(8), do: :code_proposal def parse_type(9), do: :code_approval + def parse_type(10), do: :nft @doc """ Determines if a transaction type is a network one @@ -302,11 +307,16 @@ defmodule Uniris.TransactionChain.Transaction do def get_movements(%__MODULE__{ data: %TransactionData{ ledger: %Ledger{ - uco: %UCOLedger{transfers: uco_transfers} + uco: %UCOLedger{transfers: uco_transfers}, + nft: %NFTLedger{transfers: nft_transfers} } } }) do - Enum.map(uco_transfers, &%TransactionMovement{to: &1.to, amount: &1.amount}) + Enum.map(uco_transfers, &%TransactionMovement{to: &1.to, amount: &1.amount, type: :UCO}) ++ + Enum.map( + nft_transfers, + &%TransactionMovement{to: &1.to, amount: &1.amount, type: {:NFT, &1.nft}} + ) end @doc """ @@ -446,7 +456,9 @@ defmodule Uniris.TransactionChain.Transaction do 0, 0, 0, 0, # Nb authorized keys 0, - # Nb transfers + # Nb UCO transfers + 0, + # Nb NFT transfers 0, # Nb recipients 0, @@ -588,7 +600,7 @@ defmodule Uniris.TransactionChain.Transaction do iex> <<0, 62, 198, 74, 197, 246, 83, 6, 174, 95, 223, 107, 92, 12, 36, 93, 197, 197, ...> 196, 186, 34, 34, 134, 184, 95, 181, 113, 255, 93, 134, 197, 243, 85, 2, 0, 0, 1, 115, 40, 130, 21, 209, - ...> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 250, 128, 151, 100, 231, 128, 158, 139, + ...> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 250, 128, 151, 100, 231, 128, 158, 139, ...> 88, 128, 68, 236, 240, 238, 116, 186, 164, 87, 3, 60, 198, 21, 248, 64, 207, 58, 221, 192, ...> 131, 180, 213, 64, 65, 66, 248, 246, 119, 69, 36, 103, 249, 201, 252, 154, 69, 24, 48, 18, 63, ...> 65, 5, 10, 248, 37, 245, 101, 19, 118, 235, 82, 161, 165, 62, 43, 249, 237, @@ -792,6 +804,8 @@ defmodule Uniris.TransactionChain.Transaction do @spec fee(t()) :: float() def fee(%__MODULE__{type: :node}), do: 0.0 def fee(%__MODULE__{type: :node_shared_secrets}), do: 0.0 + def fee(%__MODULE__{type: :identity}), do: 0.0 + def fee(%__MODULE__{type: :keychain}), do: 0.0 def fee(%__MODULE__{address: address}) do if address == Bootstrap.genesis_address() do diff --git a/lib/uniris/transaction_chain/transaction/data.ex b/lib/uniris/transaction_chain/transaction/data.ex index e9e12b24e..1b7e76446 100755 --- a/lib/uniris/transaction_chain/transaction/data.ex +++ b/lib/uniris/transaction_chain/transaction/data.ex @@ -49,7 +49,7 @@ defmodule Uniris.TransactionChain.TransactionData do 0, 0, 0, 147, # Code "actions do new_transaction(:transfer) |> add_uco_transfer(to: 892B5257A038BBB14F0DD8734FA09A50F4F55E8856B72F96F2A6014EEB8A2EAB72, amount: 10.5) end", - # Contente size + # Content size 0, 0, 0, 54, # Content "Lorem ipsum dolor sit amet, consectetur adipiscing eli", @@ -59,7 +59,9 @@ defmodule Uniris.TransactionChain.TransactionData do 225, 11, 213, 74, 41, 54, 189, 139, 179, 79, # Number of authorized keys 0, - # Number of uco transfers + # Number of UCO transfers + 0, + # Number of NFT transfers 0, # Number of recipients 1, @@ -94,6 +96,7 @@ defmodule Uniris.TransactionChain.TransactionData do ...> 225, 11, 213, 74, 41, 54, 189, 139, 179, 79, ...> 0, ...> 0, + ...> 0, ...> 1, ...> 0, 98, 220, 40, 53, 113, 34, 14, 142, 121, 132, 166, 27, 147, 41, 129, 195, 168, ...> 241, 217, 111, 115, 164, 99, 135, 86, 123, 17, 195, 106, 248, 173, 31 diff --git a/lib/uniris/transaction_chain/transaction/data/ledger.ex b/lib/uniris/transaction_chain/transaction/data/ledger.ex index a00ff05bd..15268398d 100755 --- a/lib/uniris/transaction_chain/transaction/data/ledger.ex +++ b/lib/uniris/transaction_chain/transaction/data/ledger.ex @@ -2,16 +2,18 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger do @moduledoc """ Represents transaction ledger movements """ + alias Uniris.TransactionChain.TransactionData.NFTLedger alias Uniris.TransactionChain.TransactionData.UCOLedger - defstruct uco: %UCOLedger{} + defstruct uco: %UCOLedger{}, nft: %NFTLedger{} @typedoc """ Ledger movements are composed from: - UCO: movements of UCO """ @type t :: %__MODULE__{ - uco: UCOLedger.t() + uco: UCOLedger.t(), + nft: NFTLedger.t() } @doc """ @@ -21,12 +23,23 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger do iex> %Ledger{ ...> uco: %UCOLedger{transfers: [ - ...> %Transfer{ + ...> %UCOLedger.Transfer{ ...> to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, ...> amount: 10.5 ...> } - ...> ]} + ...> ]}, + ...> nft: %NFTLedger{ + ...> transfers: [ + ...> %NFTLedger.Transfer{ + ...> nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, + ...> to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, + ...> amount: 10.5 + ...> } + ...> ] + ...> } ...> } ...> |> Ledger.serialize() << @@ -36,12 +49,22 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger do 0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53, # UCO Transfer amount + 64, 37, 0, 0, 0, 0, 0, 0, + # Number of NFT transfer + 1, + # NFT address from + 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175, + # NFT transfer recipient + 0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53, + # NFT transfer amount 64, 37, 0, 0, 0, 0, 0, 0 >> """ - @spec serialize(__MODULE__.t()) :: binary() - def serialize(%__MODULE__{uco: uco_ledger}) do - <> + @spec serialize(t()) :: binary() + def serialize(%__MODULE__{uco: uco_ledger, nft: nft_ledger}) do + <> end @doc """ @@ -51,13 +74,28 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger do iex> <<1, 0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53, + ...> 64, 37, 0, 0, 0, 0, 0, 0, 1, 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, + ...> 122, 206, 185, 71, 140, 74, 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175, + ...> 0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53, ...> 64, 37, 0, 0, 0, 0, 0, 0>> ...> |> Ledger.deserialize() { %Ledger{ uco: %UCOLedger{ transfers: [ - %Transfer{ + %UCOLedger.Transfer{ + to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, + amount: 10.5 + } + ] + }, + nft: %NFTLedger{ + transfers: [ + %NFTLedger.Transfer{ + nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, amount: 10.5 @@ -68,29 +106,33 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger do "" } """ - @spec deserialize(bitstring()) :: {__MODULE__.t(), bitstring()} + @spec deserialize(bitstring()) :: {t(), bitstring()} def deserialize(binary) when is_bitstring(binary) do {uco_ledger, rest} = UCOLedger.deserialize(binary) + {nft_ledger, rest} = NFTLedger.deserialize(rest) { %__MODULE__{ - uco: uco_ledger + uco: uco_ledger, + nft: nft_ledger }, rest } end - @spec from_map(map()) :: __MODULE__.t() + @spec from_map(map()) :: t() def from_map(ledger = %{}) do %__MODULE__{ - uco: Map.get(ledger, :uco, %UCOLedger{}) |> UCOLedger.from_map() + uco: Map.get(ledger, :uco, %UCOLedger{}) |> UCOLedger.from_map(), + nft: Map.get(ledger, :nft, %NFTLedger{}) |> NFTLedger.from_map() } end - @spec to_map(__MODULE__.t()) :: map() - def to_map(%__MODULE__{uco: uco}) do + @spec to_map(t()) :: map() + def to_map(%__MODULE__{uco: uco, nft: nft}) do %{ - uco: UCOLedger.to_map(uco) + uco: UCOLedger.to_map(uco), + nft: NFTLedger.to_map(nft) } end @@ -101,18 +143,29 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger do iex> %Ledger{ ...> uco: %UCOLedger{transfers: [ - ...> %Transfer{ + ...> %UCOLedger.Transfer{ ...> to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, ...> amount: 10.5 - ...> } - ...> ]} + ...> }, + ...> ]}, + ...> nft: %NFTLedger{ + ...> transfers: [ + ...> %NFTLedger.Transfer{ + ...> nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, + ...> to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, + ...> amount: 10.5 + ...> } + ...> ] + ...> } ...> } ...> |> Ledger.total_amount() - 10.5 + 21.0 """ - @spec total_amount(__MODULE__.t()) :: float() - def total_amount(%__MODULE__{uco: uco_ledger}) do - UCOLedger.total_amount(uco_ledger) + @spec total_amount(t()) :: float() + def total_amount(%__MODULE__{uco: uco_ledger, nft: nft_ledger}) do + UCOLedger.total_amount(uco_ledger) + NFTLedger.total_amount(nft_ledger) end end diff --git a/lib/uniris/transaction_chain/transaction/data/ledger/nft.ex b/lib/uniris/transaction_chain/transaction/data/ledger/nft.ex new file mode 100755 index 000000000..2fdc90d4b --- /dev/null +++ b/lib/uniris/transaction_chain/transaction/data/ledger/nft.ex @@ -0,0 +1,144 @@ +defmodule Uniris.TransactionChain.TransactionData.NFTLedger do + @moduledoc """ + Represents a NFT ledger movement + """ + defstruct transfers: [] + + alias __MODULE__.Transfer + + @typedoc """ + UCO movement is composed from: + - Transfers: List of NFT transfers + """ + @type t :: %__MODULE__{ + transfers: list(Transfer.t()) + } + + @doc """ + Serialize a NFT ledger into binary format + + ## Examples + + iex> %NFTLedger{transfers: [ + ...> %Transfer{ + ...> nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, + ...> to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, + ...> amount: 10.5 + ...> } + ...> ]} + ...> |> NFTLedger.serialize() + << + # Number of NFT transfers + 1, + # NFT address + 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175, + # NFT recipient + 0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53, + # NFT amount + 64, 37, 0, 0, 0, 0, 0, 0 + >> + """ + @spec serialize(t()) :: binary() + def serialize(%__MODULE__{transfers: transfers}) do + transfers_bin = Enum.map(transfers, &Transfer.serialize/1) |> :erlang.list_to_binary() + <> + end + + @doc """ + Deserialize an encoded NFT ledger + + ## Examples + + iex> <<1, 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175, 0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53, 64, 37, 0, 0, 0, 0, 0, 0>> + ...> |> NFTLedger.deserialize() + { + %NFTLedger{ + transfers: [ + %Transfer{ + nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, + to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, + amount: 10.5 + } + ] + }, + "" + } + """ + @spec deserialize(bitstring()) :: {t(), bitstring} + def deserialize(<<0::8, rest::bitstring>>) do + { + %__MODULE__{}, + rest + } + end + + def deserialize(<>) do + {transfers, rest} = do_reduce_transfers(rest, nb_transfers, []) + + { + %__MODULE__{ + transfers: transfers + }, + rest + } + end + + defp do_reduce_transfers(rest, nb_transfers, acc) when length(acc) == nb_transfers, + do: {Enum.reverse(acc), rest} + + defp do_reduce_transfers(binary, nb_transfers, acc) do + {transfer, rest} = Transfer.deserialize(binary) + do_reduce_transfers(rest, nb_transfers, [transfer | acc]) + end + + @spec from_map(map()) :: t() + def from_map(nft_ledger = %{}) do + %__MODULE__{ + transfers: Map.get(nft_ledger, :transfers, []) |> Enum.map(&Transfer.from_map/1) + } + end + + @spec to_map(t()) :: map() + def to_map(%__MODULE__{transfers: transfers}) do + %{ + transfers: Enum.map(transfers, &Transfer.to_map/1) + } + end + + @doc """ + Return the total of uco transferred + + ## Examples + + iex> %NFTLedger{transfers: [ + ...> %Transfer{ + ...> nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, + ...> to: <<0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, + ...> 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53>>, + ...> amount: 10.5 + ...> }, + ...> %Transfer{ + ...> nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, + ...> to: <<0, 202, 39, 113, 5, 117, 133, 141, 107, 1, 202, 156, 250, 124, 22, 13, 183, 20, + ...> 221, 181, 252, 153, 184, 2, 26, 115, 73, 148, 163, 119, 163, 86, 6>>, + ...> amount: 22.9 + ...> } + ...> ]} + ...> |> NFTLedger.total_amount() + 33.4 + """ + @spec total_amount(t()) :: float() + def total_amount(%__MODULE__{transfers: transfers}) do + Enum.reduce(transfers, 0.0, &(&2 + &1.amount)) + end +end diff --git a/lib/uniris/transaction_chain/transaction/data/ledger/nft/transfer.ex b/lib/uniris/transaction_chain/transaction/data/ledger/nft/transfer.ex new file mode 100644 index 000000000..dcf22302f --- /dev/null +++ b/lib/uniris/transaction_chain/transaction/data/ledger/nft/transfer.ex @@ -0,0 +1,110 @@ +defmodule Uniris.TransactionChain.TransactionData.NFTLedger.Transfer do + @moduledoc """ + Represents a NFT ledger transfer + """ + defstruct [:to, :amount, :nft, conditions: []] + + alias Uniris.Crypto + + @typedoc """ + Transfer is composed from: + - nft: NFT address + - to: receiver address of the asset + - amount: specify the number of NFT to transfer to the recipients + - conditions: specify to which address the NFT can be used + """ + @type t :: %__MODULE__{ + nft: binary(), + to: binary(), + amount: float(), + conditions: list(binary()) + } + + @doc """ + Serialize NFT transfer into binary format + + ## Examples + + iex> %Transfer{ + ...> nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, + ...> to: <<0, 104, 134, 142, 120, 40, 59, 99, 108, 63, 166, 143, 250, 93, 186, 216, 117, + ...> 85, 106, 43, 26, 120, 35, 44, 137, 243, 184, 160, 251, 223, 0, 93, 14>>, + ...> amount: 10.5 + ...> } + ...> |> Transfer.serialize() + << + # NFT address + 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175, + # Transfer recipient + 0, 104, 134, 142, 120, 40, 59, 99, 108, 63, 166, 143, 250, 93, 186, 216, 117, + 85, 106, 43, 26, 120, 35, 44, 137, 243, 184, 160, 251, 223, 0, 93, 14, + # Transfer amount + 64, 37, 0, 0, 0, 0, 0, 0 + >> + """ + def serialize(%__MODULE__{nft: nft, to: to, amount: amount}) do + <> + end + + @doc """ + Deserialize an encoded NFT transfer + + ## Examples + + iex> << + ...> 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175, + ...> 0, 104, 134, 142, 120, 40, 59, 99, 108, 63, 166, 143, 250, 93, 186, 216, 117, + ...> 85, 106, 43, 26, 120, 35, 44, 137, 243, 184, 160, 251, 223, 0, 93, 14, + ...> 64, 37, 0, 0, 0, 0, 0, 0>> + ...> |> Transfer.deserialize() + { + %Transfer{ + nft: <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>, + to: <<0, 104, 134, 142, 120, 40, 59, 99, 108, 63, 166, 143, 250, 93, 186, 216, 117, + 85, 106, 43, 26, 120, 35, 44, 137, 243, 184, 160, 251, 223, 0, 93, 14>>, + amount: 10.5 + }, + "" + } + """ + @spec deserialize(bitstring()) :: {t(), bitstring} + def deserialize(<>) do + nft_hash_size = Crypto.hash_size(nft_hash_id) + + <> = rest + + to_hash_size = Crypto.hash_size(to_hash_id) + <> = rest + + { + %__MODULE__{ + nft: <>, + to: <>, + amount: amount + }, + rest + } + end + + @spec from_map(map()) :: t() + def from_map(transfer = %{}) do + %__MODULE__{ + nft: Map.get(transfer, :nft), + to: Map.get(transfer, :to), + amount: Map.get(transfer, :amount) + } + end + + @spec to_map(t()) :: map() + def to_map(%__MODULE__{nft: nft, to: to, amount: amount}) do + %{ + nft: nft, + to: to, + amount: amount + } + end +end diff --git a/lib/uniris/transaction_chain/transaction/data/ledger/uco.ex b/lib/uniris/transaction_chain/transaction/data/ledger/uco.ex index 4e10e87b4..bd699b801 100755 --- a/lib/uniris/transaction_chain/transaction/data/ledger/uco.ex +++ b/lib/uniris/transaction_chain/transaction/data/ledger/uco.ex @@ -2,22 +2,20 @@ defmodule Uniris.TransactionChain.TransactionData.UCOLedger do @moduledoc """ Represents a UCO ledger movement """ - defstruct [:fee, transfers: []] + defstruct transfers: [] - alias Uniris.TransactionChain.TransactionData.Ledger.Transfer + alias __MODULE__.Transfer @typedoc """ UCO movement is composed from: - - Fee: End user can specify the fee to use (nodes will check if it's sufficient) - Transfers: List of UCO transfers """ @type t :: %__MODULE__{ - fee: float(), transfers: list(Transfer.t()) } @doc """ - Serialize uco ledger into binary format + Serialize a UCO ledger into binary format ## Examples @@ -30,23 +28,23 @@ defmodule Uniris.TransactionChain.TransactionData.UCOLedger do ...> ]} ...> |> UCOLedger.serialize() << - # Number of transfers + # Number of UCO transfers 1, - # Transfer recipient + # UCO recipient 0, 59, 140, 2, 130, 52, 88, 206, 176, 29, 10, 173, 95, 179, 27, 166, 66, 52, 165, 11, 146, 194, 246, 89, 73, 85, 202, 120, 242, 136, 136, 63, 53, - # Transfer amount + # UCO amount 64, 37, 0, 0, 0, 0, 0, 0 >> """ - @spec serialize(Uniris.TransactionChain.TransactionData.UCOLedger.t()) :: binary() + @spec serialize(t()) :: binary() def serialize(%__MODULE__{transfers: transfers}) do transfers_bin = Enum.map(transfers, &Transfer.serialize/1) |> :erlang.list_to_binary() <> end @doc """ - Deserialize an encoded uco ledger + Deserialize an encoded UCO ledger ## Examples @@ -66,7 +64,7 @@ defmodule Uniris.TransactionChain.TransactionData.UCOLedger do "" } """ - @spec deserialize(bitstring()) :: {__MODULE__.t(), bitstring} + @spec deserialize(bitstring()) :: {t(), bitstring} def deserialize(<<0::8, rest::bitstring>>) do { %__MODULE__{}, @@ -93,18 +91,16 @@ defmodule Uniris.TransactionChain.TransactionData.UCOLedger do do_reduce_transfers(rest, nb_transfers, [transfer | acc]) end - @spec from_map(map()) :: __MODULE__.t() + @spec from_map(map()) :: t() def from_map(uco_ledger = %{}) do %__MODULE__{ - fee: Map.get(uco_ledger, :fee), transfers: Map.get(uco_ledger, :transfers, []) |> Enum.map(&Transfer.from_map/1) } end - @spec to_map(__MODULE__.t()) :: map() - def to_map(%__MODULE__{fee: fee, transfers: transfers}) do + @spec to_map(t()) :: map() + def to_map(%__MODULE__{transfers: transfers}) do %{ - fee: fee, transfers: Enum.map(transfers, &Transfer.to_map/1) } end @@ -129,7 +125,7 @@ defmodule Uniris.TransactionChain.TransactionData.UCOLedger do ...> |> UCOLedger.total_amount() 33.4 """ - @spec total_amount(__MODULE__.t()) :: float() + @spec total_amount(t()) :: float() def total_amount(%__MODULE__{transfers: transfers}) do Enum.reduce(transfers, 0.0, &(&2 + &1.amount)) end diff --git a/lib/uniris/transaction_chain/transaction/data/ledger/transfer.ex b/lib/uniris/transaction_chain/transaction/data/ledger/uco/transfer.ex similarity index 72% rename from lib/uniris/transaction_chain/transaction/data/ledger/transfer.ex rename to lib/uniris/transaction_chain/transaction/data/ledger/uco/transfer.ex index 70caaf6de..43ddc0a76 100755 --- a/lib/uniris/transaction_chain/transaction/data/ledger/transfer.ex +++ b/lib/uniris/transaction_chain/transaction/data/ledger/uco/transfer.ex @@ -1,35 +1,25 @@ -defmodule Uniris.TransactionChain.TransactionData.Ledger.Transfer do +defmodule Uniris.TransactionChain.TransactionData.UCOLedger.Transfer do @moduledoc """ - Represents any ledger transfer + Represents a UCO transfer """ defstruct [:to, :amount, :conditions] alias Uniris.Crypto - @typedoc """ - Recipient address of the ledger transfers - """ - @type recipient :: binary() - - @typedoc """ - Set of conditions to spent the outputs transactions - """ - @type conditions :: list(binary()) - @typedoc """ Transfer is composed from: - - to: receiver address of the asset - - amount: specify the number of asset to transfer to the recipients - - conditions: specify to which address the asset can be used + - to: receiver address of the UCO + - amount: specify the number of UCO to transfer to the recipients + - conditions: specify to which address the UCO can be used """ @type t :: %__MODULE__{ - to: recipient(), + to: binary(), amount: float(), - conditions: conditions() + conditions: list(binary()) } @doc """ - Serialize transaction transfer into binary format + Serialize UCO transfer into binary format ## Examples @@ -40,10 +30,10 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger.Transfer do ...> } ...> |> Transfer.serialize() << - # Transfer recipient + # UCO recipient 0, 104, 134, 142, 120, 40, 59, 99, 108, 63, 166, 143, 250, 93, 186, 216, 117, 85, 106, 43, 26, 120, 35, 44, 137, 243, 184, 160, 251, 223, 0, 93, 14, - # Transfer amount + # UCO amount 64, 37, 0, 0, 0, 0, 0, 0 >> """ @@ -52,7 +42,7 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger.Transfer do end @doc """ - Deserialize an encoded transfer + Deserialize an encoded UCO transfer ## Examples @@ -81,7 +71,7 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger.Transfer do } end - @spec from_map(map()) :: __MODULE__.t() + @spec from_map(map()) :: t() def from_map(transfer = %{}) do %__MODULE__{ to: Map.get(transfer, :to), @@ -89,7 +79,7 @@ defmodule Uniris.TransactionChain.TransactionData.Ledger.Transfer do } end - @spec to_map(__MODULE__.t()) :: map() + @spec to_map(t()) :: map() def to_map(%__MODULE__{to: to, amount: amount}) do %{ to: to, diff --git a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations.ex b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations.ex index 167bdd479..03ce4e811 100644 --- a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations.ex +++ b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations.ex @@ -25,6 +25,7 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.NodeMovement alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.TransactionMovement alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput + alias Uniris.TransactionChain.TransactionData @typedoc """ - Transaction movements: represents the pending transaction ledger movements @@ -41,20 +42,54 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d @doc """ Create a new ledger operations from a pending transaction + + ## Examples + + iex> LedgerOperations.from_transaction( + ...> %Transaction{ + ...> address: "@NFT2", + ...> type: :nft, + ...> data: %TransactionData{content: "initial supply: 1000"} + ...> } + ...> ) + %LedgerOperations{ + unspent_outputs: [%UnspentOutput{from: "@NFT2", amount: 1_000, type: {:NFT, "@NFT2"}}], + fee: 0.01, + transaction_movements: [] + } """ @spec from_transaction(Transaction.t()) :: t() def from_transaction(tx = %Transaction{validation_stamp: nil, cross_validation_stamps: nil}) do - movements = - tx - |> Transaction.get_movements() - |> Enum.map(&%TransactionMovement{to: &1.to, amount: &1.amount}) - %__MODULE__{ fee: Transaction.fee(tx), - transaction_movements: movements + transaction_movements: Transaction.get_movements(tx) } + |> do_from_transaction(tx) end + defp do_from_transaction(ops, %Transaction{ + address: address, + type: :nft, + data: %TransactionData{content: content} + }) do + [[match | _]] = Regex.scan(~r/(?<=initial supply:).*\d/mi, content) + + initial_supply = + match + |> String.trim() + |> String.replace(" ", "") + |> String.to_integer() + + %{ + ops + | unspent_outputs: [ + %UnspentOutput{from: address, amount: initial_supply, type: {:NFT, address}} + ] + } + end + + defp do_from_transaction(ops, _), do: ops + @doc """ Create node rewards and movements based on the transaction fee by distributing it using the different rates for each individual actor: welcome node, coordinator node, cross validation node, previous storage node @@ -248,38 +283,28 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d iex> %LedgerOperations{ ...> transaction_movements: [ - ...> %TransactionMovement{to: "@Bob4", amount: 10.4}, - ...> %TransactionMovement{to: "@Charlie2", amount: 2.17} + ...> %TransactionMovement{to: "@Bob4", amount: 10.4, type: :UCO}, + ...> %TransactionMovement{to: "@Charlie2", amount: 2.17, type: :UCO}, + ...> %TransactionMovement{to: "@Charlie2", amount: 200.0, type: {:NFT, "@TomNFT"}}, ...> ], ...> fee: 0.40 ...> } ...> |> LedgerOperations.total_to_spend() - 12.97 + %{ uco: 12.97, nft: %{ "@TomNFT" => 200.0 } } """ - @spec total_to_spend(t()) :: float() + @spec total_to_spend(t()) :: %{:uco => float(), :nft => %{binary() => float()}} def total_to_spend(%__MODULE__{transaction_movements: transaction_movements, fee: fee}) do - uco_to_spend = Enum.reduce(transaction_movements, 0.0, &(&2 + &1.amount)) - uco_to_spend + fee + ledger_balances(transaction_movements, %{uco: fee, nft: %{}}) end - @doc """ - Determine if the funds are sufficient with the given amount of unspent outputs for total of uco to spend - - ## Examples + defp ledger_balances(movements, acc \\ %{uco: 0.0, nft: %{}}) do + Enum.reduce(movements, acc, fn + %{type: :UCO, amount: amount}, acc -> + Map.update!(acc, :uco, &(&1 + amount)) - iex> %LedgerOperations{ - ...> transaction_movements: [ - ...> %TransactionMovement{to: "@Bob4", amount: 10.4}, - ...> %TransactionMovement{to: "@Charlie2", amount: 2.17} - ...> ], - ...> fee: 0.40 - ...> } - ...> |> LedgerOperations.enough_funds?(20.0) - true - """ - @spec enough_funds?(t(), float()) :: boolean() - def enough_funds?(ops = %__MODULE__{}, amount) when is_float(amount) do - total_to_spend(ops) <= amount + %{type: {:NFT, nft_address}, amount: amount}, acc -> + update_in(acc, [:nft, Access.key(nft_address, 0.0)], &(&1 + amount)) + end) end @doc """ @@ -289,8 +314,9 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d iex> %LedgerOperations{ ...> transaction_movements: [ - ...> %TransactionMovement{to: "@Bob4", amount: 10.4}, - ...> %TransactionMovement{to: "@Charlie2", amount: 2.17} + ...> %TransactionMovement{to: "@Bob4", amount: 10.4, type: :UCO}, + ...> %TransactionMovement{to: "@Charlie2", amount: 2.17, type: :UCO}, + ...> %TransactionMovement{to: "@Tom4", amount: 5, type: {:NFT, "@BobNFT"}} ...> ], ...> fee: 0.40 ...> } @@ -299,18 +325,42 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d iex> %LedgerOperations{ ...> transaction_movements: [ - ...> %TransactionMovement{to: "@Bob4", amount: 10.4}, - ...> %TransactionMovement{to: "@Charlie2", amount: 2.17} + ...> %TransactionMovement{to: "@Bob4", amount: 10.4, type: :UCO}, + ...> %TransactionMovement{to: "@Charlie2", amount: 2.17, type: :UCO}, + ...> %TransactionMovement{to: "@Tom4", amount: 5, type: {:NFT, "@BobNFT"}} ...> ], ...> fee: 0.40 ...> } - ...> |> LedgerOperations.sufficient_funds?([%UnspentOutput{from: "@Charlie5", amount: 30}]) + ...> |> LedgerOperations.sufficient_funds?([ + ...> %UnspentOutput{from: "@Charlie5", amount: 30, type: :UCO}, + ...> %UnspentOutput{from: "@Bob4", amount: 10, type: {:NFT, "@BobNFT"}} + ...> ]) true """ @spec sufficient_funds?(t(), list(UnspentOutput.t() | TransactionInput.t())) :: boolean() def sufficient_funds?(operations = %__MODULE__{}, inputs) when is_list(inputs) do - amount_received = Enum.reduce(inputs, 0.0, &(&2 + &1.amount)) - amount_received >= total_to_spend(operations) + %{uco: uco_balance, nft: nfts_received} = ledger_balances(inputs) + %{uco: uco_to_spend, nft: nfts_to_spend} = total_to_spend(operations) + uco_balance >= uco_to_spend and sufficient_nfts?(nfts_received, nfts_to_spend) + end + + defp sufficient_nfts?(nfts_received = %{}, nft_to_spend = %{}) + when map_size(nfts_received) == 0 and map_size(nft_to_spend) > 0, + do: false + + defp sufficient_nfts?(nfts_received, nfts_to_spend) do + Enum.all?(nfts_received, fn {nft_address, recv_amount} -> + case Map.get(nfts_to_spend, nft_address) do + nil -> + false + + amount_to_spend when recv_amount >= amount_to_spend -> + true + + _ -> + false + end + end) end @doc """ @@ -320,23 +370,106 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d ## Examples + # When a single unspent output is sufficient to satisfy the transaction movements + + iex> %LedgerOperations{ + ...> transaction_movements: [ + ...> %TransactionMovement{to: "@Bob4", amount: 10.4, type: :UCO}, + ...> %TransactionMovement{to: "@Charlie2", amount: 2.17, type: :UCO} + ...> ], + ...> fee: 0.40 + ...> } + ...> |> LedgerOperations.consume_inputs("@Alice2", [ + ...> %UnspentOutput{from: "@Bob3", amount: 20, type: :UCO} + ...> ]) + %LedgerOperations{ + transaction_movements: [ + %TransactionMovement{to: "@Bob4", amount: 10.4, type: :UCO}, + %TransactionMovement{to: "@Charlie2", amount: 2.17, type: :UCO} + ], + fee: 0.40, + node_movements: [], + unspent_outputs: [ + %UnspentOutput{from: "@Alice2", amount: 7.029999999999999, type: :UCO} + ] + } + + # When multiple little unspent output are sufficient to satisfy the transaction movements + iex> %LedgerOperations{ ...> transaction_movements: [ - ...> %TransactionMovement{to: "@Bob4", amount: 10.4}, - ...> %TransactionMovement{to: "@Charlie2", amount: 2.17} + ...> %TransactionMovement{to: "@Bob4", amount: 10.4, type: :UCO}, + ...> %TransactionMovement{to: "@Charlie2", amount: 2.17, type: :UCO}, ...> ], ...> fee: 0.40 ...> } - ...> |> LedgerOperations.consume_inputs("@Alice2", [%UnspentOutput{from: "@Bob3", amount: 20}]) + ...> |> LedgerOperations.consume_inputs("@Alice2", [ + ...> %UnspentOutput{from: "@Bob3", amount: 5, type: :UCO}, + ...> %UnspentOutput{from: "@Tom4", amount: 7, type: :UCO}, + ...> %UnspentOutput{from: "@Christina", amount: 4, type: :UCO}, + ...> %UnspentOutput{from: "@Hugo", amount: 8, type: :UCO} + ...> ]) %LedgerOperations{ transaction_movements: [ - %TransactionMovement{to: "@Bob4", amount: 10.4}, - %TransactionMovement{to: "@Charlie2", amount: 2.17} + %TransactionMovement{to: "@Bob4", amount: 10.4, type: :UCO}, + %TransactionMovement{to: "@Charlie2", amount: 2.17, type: :UCO} ], fee: 0.40, node_movements: [], unspent_outputs: [ - %UnspentOutput{from: "@Alice2", amount: 7.029999999999999} + %UnspentOutput{from: "@Alice2", amount: 3.0299999999999994, type: :UCO}, + %UnspentOutput{from: "@Hugo", amount: 8, type: :UCO} + ] + } + + # When using NFT unspent outputs are sufficient to satisfy the transaction movements + + iex> %LedgerOperations{ + ...> transaction_movements: [ + ...> %TransactionMovement{to: "@Bob4", amount: 10.4, type: {:NFT, "@CharlieNFT"}}, + ...> ], + ...> fee: 0.40 + ...> } + ...> |> LedgerOperations.consume_inputs("@Alice2", [ + ...> %UnspentOutput{from: "@Charlie1", amount: 2.0, type: :UCO}, + ...> %UnspentOutput{from: "@Bob3", amount: 12, type: {:NFT, "@CharlieNFT"}} + ...> ]) + %LedgerOperations{ + transaction_movements: [ + %TransactionMovement{to: "@Bob4", amount: 10.4, type: {:NFT, "@CharlieNFT"}} + ], + fee: 0.40, + node_movements: [], + unspent_outputs: [ + %UnspentOutput{from: "@Alice2", amount: 1.60, type: :UCO}, + %UnspentOutput{from: "@Alice2", amount: 1.5999999999999996, type: {:NFT, "@CharlieNFT"}} + ] + } + + # When multiple NFT unspent outputs are sufficient to satisfy the transaction movements + + iex> %LedgerOperations{ + ...> transaction_movements: [ + ...> %TransactionMovement{to: "@Bob4", amount: 10.4, type: {:NFT, "@CharlieNFT"}}, + ...> ], + ...> fee: 0.40 + ...> } + ...> |> LedgerOperations.consume_inputs("@Alice2", [ + ...> %UnspentOutput{from: "@Charlie1", amount: 2.0, type: :UCO}, + ...> %UnspentOutput{from: "@Bob3", amount: 5, type: {:NFT, "@CharlieNFT"}}, + ...> %UnspentOutput{from: "@Hugo5", amount: 7, type: {:NFT, "@CharlieNFT"}}, + ...> %UnspentOutput{from: "@Tom1", amount: 7, type: {:NFT, "@CharlieNFT"}} + ...> ]) + %LedgerOperations{ + transaction_movements: [ + %TransactionMovement{to: "@Bob4", amount: 10.4, type: {:NFT, "@CharlieNFT"}} + ], + fee: 0.40, + node_movements: [], + unspent_outputs: [ + %UnspentOutput{from: "@Alice2", amount: 1.60, type: :UCO}, + %UnspentOutput{from: "@Alice2", amount: 1.5999999999999996, type: {:NFT, "@CharlieNFT"}}, + %UnspentOutput{from: "@Hugo5", amount: 7, type: {:NFT, "@CharlieNFT"}} ] } """ @@ -346,57 +479,139 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d inputs :: UnspentOutput.t() | TransactionInput.t() ) :: t() - def consume_inputs(ops = %__MODULE__{}, change_address, inputs) do - uco_amount = Enum.reduce(inputs, 0.0, &(&2 + &1.amount)) - - if uco_amount >= total_to_spend(ops) do - sorted_inputs = Enum.sort_by(inputs, & &1.amount) - + def consume_inputs(ops = %__MODULE__{}, change_address, inputs) + when is_binary(change_address) and is_list(inputs) do + if sufficient_funds?(ops, inputs) do new_unspent_outputs = - do_consume_inputs( - change_address, - Enum.map(sorted_inputs, &%UnspentOutput{from: &1.from, amount: &1.amount}), - total_to_spend(ops), - 0.0 - ) + make_new_unspent_outputs(change_address, sort_inputs(inputs), total_to_spend(ops), %{ + uco: 0.0, + nft: %{} + }) - %{ops | unspent_outputs: new_unspent_outputs} + Map.update(ops, :unspent_outputs, new_unspent_outputs, &(new_unspent_outputs ++ &1)) else - %{ops | unspent_outputs: []} + ops end end - defp do_consume_inputs(change_address, unspent_outputs, remaining, change) - when remaining == 0.0 and change > 0.0 do - [%UnspentOutput{amount: change, from: change_address} | unspent_outputs] + defp sort_inputs(inputs) do + sorted_uco_inputs = + inputs + |> Enum.filter(&(&1.type == :UCO)) + |> Enum.sort_by(& &1.amount) + + sorted_nft_inputs = + inputs + |> Enum.filter(&match?({:NFT, _}, &1.type)) + |> Enum.reduce(%{}, fn input = %{type: {:NFT, nft_address}}, acc -> + Map.update(acc, nft_address, [input], fn nft_acc -> + Enum.sort_by([input | nft_acc], & &1.amount) + end) + end) + |> Map.values() + |> :lists.flatten() + + sorted_uco_inputs ++ sorted_nft_inputs end - defp do_consume_inputs(_change_address, unspent_outputs, remaining, _change) - when remaining == 0.0, - do: unspent_outputs + defp make_new_unspent_outputs( + change_address, + unspent_outputs, + remaning = %{uco: uco_remaining, nft: nft_remaining}, + change = %{uco: uco_change, nft: nft_change} + ) do + cond do + uco_remaining == 0.0 and uco_change > 0.0 and + (Enum.all?(nft_remaining, fn {_, amount} -> amount == 0.0 end) and + Enum.all?(nft_change, fn {_, amount} -> amount > 0.0 end)) -> + change_uco = %UnspentOutput{amount: uco_change, from: change_address, type: :UCO} + + change_nfts = + Enum.map(nft_change, fn {nft_address, amount} -> + %UnspentOutput{amount: amount, from: change_address, type: {:NFT, nft_address}} + end) + + [change_uco | change_nfts] ++ unspent_outputs + + uco_remaining == 0.0 and Enum.all?(nft_remaining, fn {_, amount} -> amount == 0.0 end) -> + unspent_outputs + + true -> + do_consume_inputs(change_address, unspent_outputs, remaning, change) + end + end - # When a full unspent output is sufficient for the entire amount to spend + # When a full UCO unspent output is sufficient for the entire amount to spend # The unspent output is fully consumed and remaining part is return as changed defp do_consume_inputs( change_address, - [%UnspentOutput{amount: amount} | rest], - remaining, + [%{amount: amount, type: :UCO} | rest], + remaining = %{uco: remaining_uco}, change ) - when amount >= remaining do - do_consume_inputs(change_address, rest, 0.0, change + (amount - remaining)) + when amount >= remaining_uco do + make_new_unspent_outputs( + change_address, + rest, + Map.put(remaining, :uco, 0.0), + Map.update!(change, :uco, &(&1 + (amount - remaining_uco))) + ) end - # When a the unspent_output is a part of the amount to spend + # When a the UCO unspent_output is a part of the amount to spend # The unspent_output is fully consumed and the iteration continue utils the the remaining amount to spend are consumed defp do_consume_inputs( change_address, - [%UnspentOutput{amount: amount} | rest], - remaining, + [%{amount: amount, type: :UCO} | rest], + remaining = %{uco: remaining_uco}, change ) - when amount < remaining do - do_consume_inputs(change_address, rest, abs(remaining - amount), change) + when amount < remaining_uco do + make_new_unspent_outputs( + change_address, + rest, + Map.put(remaining, :uco, abs(remaining_uco - amount)), + change + ) + end + + defp do_consume_inputs( + change_address, + unspent_outputs = [%{amount: amount, type: {:NFT, nft_address}} | rest], + remaining = %{nft: nft_remaining}, + change + ) do + remaining_nft_amount = Map.get(nft_remaining, nft_address) + + cond do + # When a full NFT unspent output is sufficient for the entire amount to spend + # The unspent output is fully consumed and remaining part is return as changed + amount >= remaining_nft_amount -> + make_new_unspent_outputs( + change_address, + rest, + put_in(remaining, [:nft, nft_address], 0.0), + update_in( + change, + [:nft, Access.key(nft_address, 0.0)], + &(&1 + (amount - remaining_nft_amount)) + ) + ) + + # When a the UCO unspent_output is a part of the amount to spend + # The unspent_output is fully consumed and the iteration continue until + # the remaining amount to spend are consumed + amount < remaining_nft_amount -> + make_new_unspent_outputs( + change_address, + rest, + update_in(remaining, [:nft, Access.key(nft_address, 0.0)], &abs(&1 - amount)), + change + ) + + true -> + make_new_unspent_outputs(change_address, unspent_outputs, remaining, change) + end end @doc """ @@ -409,7 +624,7 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d iex> %LedgerOperations{ ...> transaction_movements: [ ...> %TransactionMovement{to: <<0, 167, 158, 251, 11, 241, 12, 240, 78, 125, 145, 72, 181, 180, 207, 109, 100, - ...> 239, 164, 17, 54, 91, 246, 111, 162, 112, 35, 174, 44, 92, 45, 57, 213>>, amount: 5.3} + ...> 239, 164, 17, 54, 91, 246, 111, 162, 112, 35, 174, 44, 92, 45, 57, 213>>, amount: 5.3, type: :UCO} ...> ], ...> node_movements: [ ...> %NodeMovement{to: <<82, 181, 95, 101, 84, 42, 93, 217, 66, 3, 234, 7, 7, 100, 88, 24, 65, 146, 60, @@ -443,7 +658,8 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d ...> %TransactionMovement{ ...> to: <<0, 34, 118, 242, 194, 93, 131, 130, 195, 9, 97, 237, 220, 195, 112, 1, 54, 221, ...> 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, 158, 139, 207>>, - ...> amount: 10.2 + ...> amount: 10.2, + ...> type: :UCO ...> } ...> ], ...> node_movements: [ @@ -458,7 +674,8 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d ...> %UnspentOutput{ ...> from: <<0, 34, 118, 242, 194, 93, 131, 130, 195, 9, 97, 237, 220, 195, 112, 1, 54, 221, ...> 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, 158, 139, 207>>, - ...> amount: 2.0 + ...> amount: 2.0, + ...> type: :UCO ...> } ...> ] ...> } @@ -473,6 +690,8 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, 158, 139, 207, # Transaction movement amount "@$ffffff", + # Transaction movement type (UCO) + 0, # Nb of node movements 1, # Node public key @@ -490,7 +709,9 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d 0, 34, 118, 242, 194, 93, 131, 130, 195, 9, 97, 237, 220, 195, 112, 1, 54, 221, 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, 158, 139, 207, # Unspent output amount - 64, 0, 0, 0, 0, 0, 0, 0 + 64, 0, 0, 0, 0, 0, 0, 0, + ## Unspent output type (UCO) + 0 >> """ def serialize(%__MODULE__{ @@ -522,11 +743,11 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d iex> <<63, 185, 153, 153, 153, 153, 153, 154, 1, 0, 34, 118, 242, 194, 93, 131, 130, 195, ...> 9, 97, 237, 220, 195, 112, 1, 54, 221, 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, - ...> 158, 139, 207, "@$ffffff", 1, 0, 34, 118, 242, 194, 93, 131, 130, 195, 9, 97, 237, 220, 195, 112, + ...> 158, 139, 207, "@$ffffff", 0, 1, 0, 34, 118, 242, 194, 93, 131, 130, 195, 9, 97, 237, 220, 195, 112, ...> 1, 54, 221, 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, 158, 139, 207, ...> 63, 211, 51, 51, 51, 51, 51, 51, 4, 0, 1, 2, 3, 1, 0, 34, 118, 242, 194, 93, 131, 130, 195, 9, 97, 237, ...> 220, 195, 112, 1, 54, 221, 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, 158, 139, 207, - ...> 64, 0, 0, 0, 0, 0, 0, 0 >> + ...> 64, 0, 0, 0, 0, 0, 0, 0, 0 >> ...> |> LedgerOperations.deserialize() { %LedgerOperations{ @@ -535,7 +756,8 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d %TransactionMovement{ to: <<0, 34, 118, 242, 194, 93, 131, 130, 195, 9, 97, 237, 220, 195, 112, 1, 54, 221, 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, 158, 139, 207>>, - amount: 10.2 + amount: 10.2, + type: :UCO } ], node_movements: [ @@ -550,7 +772,8 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations d %UnspentOutput{ from: <<0, 34, 118, 242, 194, 93, 131, 130, 195, 9, 97, 237, 220, 195, 112, 1, 54, 221, 86, 154, 234, 96, 217, 149, 84, 188, 63, 242, 166, 47, 158, 139, 207>>, - amount: 2.0 + amount: 2.0, + type: :UCO } ] }, diff --git a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/node_movement.ex b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/node_movement.ex index d7d88a9ca..bb5f0afac 100644 --- a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/node_movement.ex +++ b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/node_movement.ex @@ -3,7 +3,6 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.N Represents the movements regarding the nodes involved during the transaction validation. The node public keys are present as well as their rewards """ - @enforce_keys [:to, :amount, :roles] defstruct [:to, :amount, :roles] alias Uniris.Crypto diff --git a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/transaction_movement.ex b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/transaction_movement.ex index 291525bce..d156ecfa8 100644 --- a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/transaction_movement.ex +++ b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/transaction_movement.ex @@ -3,14 +3,21 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.T Represents the ledger movements of the transaction extracted from the ledger or recipients part of the transaction and validated with the unspent outputs """ - @enforce_keys [:to, :amount] - defstruct [:to, :amount] + defstruct [:to, :amount, :type] + alias __MODULE__.Type alias Uniris.Crypto + @typedoc """ + TransactionMovement is composed from: + - to: receiver address of the movement + - amount: specify the number assets to transfer to the recipients + - type: asset type (ie. UCO or NFT) + """ @type t() :: %__MODULE__{ to: Crypto.versioned_hash(), - amount: float() + amount: float(), + type: Type.t() } @doc """ @@ -21,7 +28,26 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.T iex> %TransactionMovement{ ...> to: <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, ...> 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186>>, - ...> amount: 0.30 + ...> amount: 0.30, + ...> type: :UCO + ...> } + ...> |> TransactionMovement.serialize() + << + # Node public key + 0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, + 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186, + # Amount + 63, 211, 51, 51, 51, 51, 51, 51, + # UCO type + 0 + >> + + iex> %TransactionMovement{ + ...> to: <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, + ...> 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186>>, + ...> amount: 0.30, + ...> type: {:NFT, <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>} ...> } ...> |> TransactionMovement.serialize() << @@ -29,12 +55,17 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.T 0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186, # Amount - 63, 211, 51, 51, 51, 51, 51, 51 + 63, 211, 51, 51, 51, 51, 51, 51, + # NFT type + 1, + # NFT address + 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175 >> """ - @spec serialize(__MODULE__.t()) :: <<_::64, _::_*8>> - def serialize(%__MODULE__{to: to, amount: amount}) do - <> + @spec serialize(t()) :: <<_::64, _::_*8>> + def serialize(%__MODULE__{to: to, amount: amount, type: type}) do + <> end @doc """ @@ -44,45 +75,87 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.T iex> <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, ...> 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186, - ...> 63, 211, 51, 51, 51, 51, 51, 51 + ...> 63, 211, 51, 51, 51, 51, 51, 51, 0 + ...> >> + ...> |> TransactionMovement.deserialize() + { + %TransactionMovement{ + to: <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, + 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186>>, + amount: 0.30, + type: :UCO + }, + "" + } + + iex> <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, + ...> 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186, + ...> 63, 211, 51, 51, 51, 51, 51, 51, 1, 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175 ...> >> ...> |> TransactionMovement.deserialize() { %TransactionMovement{ to: <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186>>, - amount: 0.30 + amount: 0.30, + type: {:NFT, <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>} }, "" } """ - @spec deserialize(bitstring()) :: {__MODULE__.t(), bitstring} + @spec deserialize(bitstring()) :: {t(), bitstring} def deserialize(<>) do hash_size = Crypto.hash_size(hash_id) <> = rest + {type, rest} = Type.deserialize(rest) { %__MODULE__{ to: <> <> address, - amount: amount + amount: amount, + type: type }, rest } end - @spec from_map(map()) :: __MODULE__.t() + @spec from_map(map()) :: t() def from_map(movement = %{}) do - %__MODULE__{ + res = %__MODULE__{ to: Map.get(movement, :to), amount: Map.get(movement, :amount) } + + if Map.has_key?(movement, :type) do + case Map.get(movement, :nft_address) do + nil -> + %{res | type: :UCO} + + nft_address -> + %{res | type: {:NFT, nft_address}} + end + else + res + end + end + + @spec to_map(t()) :: map() + def to_map(%__MODULE__{to: to, amount: amount, type: :UCO}) do + %{ + to: to, + amount: amount, + type: :UCO + } end - @spec to_map(__MODULE__.t()) :: map() - def to_map(%__MODULE__{to: to, amount: amount}) do + def to_map(%__MODULE__{to: to, amount: amount, type: {:NFT, nft_address}}) do %{ to: to, - amount: amount + amount: amount, + type: :NFT, + nft_address: nft_address } end end diff --git a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/transaction_movement/type.ex b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/transaction_movement/type.ex new file mode 100644 index 000000000..4e1e41287 --- /dev/null +++ b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/transaction_movement/type.ex @@ -0,0 +1,28 @@ +defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.TransactionMovement.Type do + @moduledoc """ + Represents a type of transaction movement. + """ + + alias Uniris.Crypto + + @typedoc """ + Transaction movement can be: + - UCO transfers + - NFT transfers. When it's a NFT transfer, the type indicates the address of NFT to transfer + """ + @type t() :: :UCO | {:NFT, Crypto.versioned_hash()} + + def serialize(:UCO), do: <<0>> + + def serialize({:NFT, address}) do + <<1::8, address::binary>> + end + + def deserialize(<<0::8, rest::bitstring>>), do: {:UCO, rest} + + def deserialize(<<1::8, hash_id::8, rest::bitstring>>) do + hash_size = Crypto.hash_size(hash_id) + <> = rest + {{:NFT, <>}, rest} + end +end diff --git a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/unspent_output.ex b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/unspent_output.ex index d7158e19f..ba5f3580b 100644 --- a/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/unspent_output.ex +++ b/lib/uniris/transaction_chain/transaction/validation_stamp/ledger_operations/unspent_output.ex @@ -2,14 +2,17 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.U @moduledoc """ Represents an unspent output from a transaction. """ - @enforce_keys [:amount, :from] - defstruct [:amount, :from] + defstruct [:amount, :from, :type] alias Uniris.Crypto + alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.TransactionMovement.Type, + as: TransactionMovementType + @type t :: %__MODULE__{ amount: float(), - from: Crypto.versioned_hash() + from: Crypto.versioned_hash(), + type: TransactionMovementType.t() } @doc """ @@ -17,10 +20,13 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.U ## Examples + With UCO movements: + iex> %UnspentOutput{ ...> from: <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, ...> 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186>>, - ...> amount: 10.5 + ...> amount: 10.5, + ...> type: :UCO ...> } ...> |> UnspentOutput.serialize() << @@ -28,12 +34,37 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.U 0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186, # Amount - 64, 37, 0, 0, 0, 0, 0, 0 + 64, 37, 0, 0, 0, 0, 0, 0, + # UCO Unspent Output + 0 + >> + + With NFT movements: + + iex> %UnspentOutput{ + ...> from: <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, + ...> 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186>>, + ...> amount: 10.5, + ...> type: {:NFT, <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>} + ...> } + ...> |> UnspentOutput.serialize() + << + # From + 0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, + 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186, + # Amount + 64, 37, 0, 0, 0, 0, 0, 0, + # NFT Unspent Output + 1, + # NFT address + 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175 >> """ @spec serialize(__MODULE__.t()) :: <<_::64, _::_*8>> - def serialize(%__MODULE__{from: from, amount: amount}) do - <> + def serialize(%__MODULE__{from: from, amount: amount, type: type}) do + <> end @doc """ @@ -43,14 +74,32 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.U iex> <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, ...> 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186, - ...> 64, 37, 0, 0, 0, 0, 0, 0 + ...> 64, 37, 0, 0, 0, 0, 0, 0, 0 + ...> >> + ...> |> UnspentOutput.deserialize() + { + %UnspentOutput{ + from: <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, + 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186>>, + amount: 10.5, + type: :UCO + }, + "" + } + + iex> <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, + ...> 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186, + ...> 64, 37, 0, 0, 0, 0, 0, 0, 1, 0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + ...> 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175 ...> >> ...> |> UnspentOutput.deserialize() { %UnspentOutput{ from: <<0, 214, 107, 17, 107, 227, 11, 17, 43, 204, 48, 78, 129, 145, 126, 45, 68, 194, 159, 19, 92, 240, 29, 37, 105, 183, 232, 56, 42, 163, 236, 251, 186>>, - amount: 10.5 + amount: 10.5, + type: {:NFT, <<0, 49, 101, 72, 154, 152, 3, 174, 47, 2, 35, 7, 92, 122, 206, 185, 71, 140, 74, + 197, 46, 99, 117, 89, 96, 100, 20, 0, 34, 181, 215, 143, 175>>} }, "" } @@ -59,29 +108,53 @@ defmodule Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.U def deserialize(<>) do hash_size = Crypto.hash_size(hash_id) <> = rest + {type, rest} = TransactionMovementType.deserialize(rest) { %__MODULE__{ from: <> <> address, - amount: amount + amount: amount, + type: type }, rest } end @spec from_map(map()) :: __MODULE__.t() - def from_map(utxo = %{}) do - %__MODULE__{ - from: Map.get(utxo, :from), - amount: Map.get(utxo, :amount) + def from_map(unspent_output = %{}) do + res = %__MODULE__{ + from: Map.get(unspent_output, :from), + amount: Map.get(unspent_output, :amount) + } + + if Map.has_key?(unspent_output, :type) do + case Map.get(unspent_output, :nft_address) do + nil -> + %{res | type: :UCO} + + nft_address -> + %{res | type: {:NFT, nft_address}} + end + else + res + end + end + + @spec to_map(t()) :: map() + def to_map(%__MODULE__{from: from, amount: amount, type: :UCO}) do + %{ + from: from, + amount: amount, + type: :UCO } end - @spec to_map(__MODULE__.t()) :: map() - def to_map(%__MODULE__{from: from, amount: amount}) do + def to_map(%__MODULE__{from: from, amount: amount, type: {:NFT, nft_address}}) do %{ from: from, - amount: amount + amount: amount, + type: :NFT, + nft_address: nft_address } end end diff --git a/lib/uniris/transaction_chain/transaction_input.ex b/lib/uniris/transaction_chain/transaction_input.ex index 1c67abb8b..a5a724116 100644 --- a/lib/uniris/transaction_chain/transaction_input.ex +++ b/lib/uniris/transaction_chain/transaction_input.ex @@ -2,14 +2,18 @@ defmodule Uniris.TransactionChain.TransactionInput do @moduledoc """ Represents an transaction sent to an account either spent or unspent """ - defstruct [:from, :amount, :spent?] + defstruct [:from, :amount, :type, :spent?] alias Uniris.Crypto + alias Uniris.TransactionChain.Transaction.ValidationStamp.LedgerOperations.TransactionMovement.Type, + as: TransactionMovementType + @type t() :: %__MODULE__{ from: Crypto.versioned_hash(), amount: float(), - spent?: boolean() + spent?: boolean(), + type: TransactionMovementType.t() } @doc """ @@ -21,6 +25,7 @@ defmodule Uniris.TransactionChain.TransactionInput do ...> from: <<0, 53, 130, 31, 59, 131, 78, 78, 34, 179, 66, 2, 120, 117, 4, 119, 81, 111, 187, ...> 166, 83, 194, 42, 253, 99, 189, 24, 68, 40, 178, 142, 163, 56>>, ...> amount: 10.5, + ...> type: :UCO, ...> spent?: true ...> } ...> |> TransactionInput.serialize() @@ -30,17 +35,19 @@ defmodule Uniris.TransactionChain.TransactionInput do 166, 83, 194, 42, 253, 99, 189, 24, 68, 40, 178, 142, 163, 56, # Amount 64, 37, 0, 0, 0, 0, 0, 0, + # Input type (UCO) + 0, # Spent 1::1 >> """ @spec serialize(__MODULE__.t()) :: bitstring() - def serialize(%__MODULE__{from: from, amount: amount, spent?: true}) do - <> + def serialize(%__MODULE__{from: from, amount: amount, type: type, spent?: true}) do + <> end - def serialize(%__MODULE__{from: from, amount: amount, spent?: _}) do - <> + def serialize(%__MODULE__{from: from, amount: amount, type: type, spent?: _}) do + <> end @doc """ @@ -50,13 +57,14 @@ defmodule Uniris.TransactionChain.TransactionInput do iex> <<0, 53, 130, 31, 59, 131, 78, 78, 34, 179, 66, 2, 120, 117, 4, 119, 81, 111, 187, ...> 166, 83, 194, 42, 253, 99, 189, 24, 68, 40, 178, 142, 163, 56, - ...> 64, 37, 0, 0, 0, 0, 0, 0, 1::1>> + ...> 64, 37, 0, 0, 0, 0, 0, 0, 0, 1::1>> ...> |> TransactionInput.deserialize() { %TransactionInput{ from: <<0, 53, 130, 31, 59, 131, 78, 78, 34, 179, 66, 2, 120, 117, 4, 119, 81, 111, 187, 166, 83, 194, 42, 253, 99, 189, 24, 68, 40, 178, 142, 163, 56>>, amount: 10.5, + type: :UCO, spent?: true }, "" @@ -65,23 +73,27 @@ defmodule Uniris.TransactionChain.TransactionInput do @spec deserialize(bitstring()) :: {__MODULE__.t(), bitstring()} def deserialize(<>) do hash_size = Crypto.hash_size(hash_id) + <> = rest + {type, rest} = TransactionMovementType.deserialize(rest) case rest do - <> -> + <<0::1, rest::bitstring>> -> { %__MODULE__{ from: <>, amount: amount, + type: type, spent?: false }, rest } - <> -> + <<1::1, rest::bitstring>> -> { %__MODULE__{ from: <>, amount: amount, + type: type, spent?: true }, rest @@ -91,18 +103,41 @@ defmodule Uniris.TransactionChain.TransactionInput do @spec from_map(map()) :: __MODULE__.t() def from_map(input = %{}) do - %__MODULE__{ + res = %__MODULE__{ amount: Map.get(input, :amount), from: Map.get(input, :from), spent?: Map.get(input, :spent?) } + + if Map.has_key?(input, :type) do + case Map.get(input, :nft_address) do + nil -> + %{res | type: :UCO} + + nft_address -> + %{res | type: {:NFT, nft_address}} + end + else + res + end end @spec to_map(__MODULE__.t()) :: map() - def to_map(%__MODULE__{amount: amount, from: from, spent?: spent?}) do + def to_map(%__MODULE__{amount: amount, from: from, spent?: spent?, type: :UCO}) do + %{ + amount: amount, + from: from, + type: :UCO, + spent?: spent? + } + end + + def to_map(%__MODULE__{amount: amount, from: from, spent?: spent?, type: {:NFT, nft_address}}) do %{ amount: amount, from: from, + type: :NFT, + nft_address: nft_address, spent?: spent? } end diff --git a/lib/uniris_web.ex b/lib/uniris_web.ex index 3d8dc4302..e1d9f5c41 100644 --- a/lib/uniris_web.ex +++ b/lib/uniris_web.ex @@ -55,7 +55,6 @@ defmodule UnirisWeb do import UnirisWeb.ErrorHelpers import UnirisWeb.LayoutHelpers - import UnirisWeb.ComponentHelpers alias UnirisWeb.Router.Helpers, as: Routes end diff --git a/lib/uniris_web/controllers/api/transaction_controller.ex b/lib/uniris_web/controllers/api/transaction_controller.ex index 0a3b091f6..34f591c74 100644 --- a/lib/uniris_web/controllers/api/transaction_controller.ex +++ b/lib/uniris_web/controllers/api/transaction_controller.ex @@ -9,102 +9,54 @@ defmodule UnirisWeb.API.TransactionController do alias Uniris.TransactionChain.TransactionData alias Uniris.TransactionChain.TransactionData.Keys alias Uniris.TransactionChain.TransactionData.Ledger - alias Uniris.TransactionChain.TransactionData.Ledger.Transfer + alias Uniris.TransactionChain.TransactionData.NFTLedger + alias Uniris.TransactionChain.TransactionData.NFTLedger.Transfer, as: NFTTransfer alias Uniris.TransactionChain.TransactionData.UCOLedger + alias Uniris.TransactionChain.TransactionData.UCOLedger.Transfer, as: UCOTransfer - def new( - conn, - _params = %{ - "address" => address, - "type" => type, - "timestamp" => timestamp, - "data" => %{ - "code" => code, - "content" => content, - "keys" => %{ - "secret" => secret, - "authorizedKeys" => authorized_keys - }, - "ledger" => %{ - "uco" => %{ - "transfers" => transfers - } - }, - "recipients" => recipients - }, - "previousPublicKey" => previous_public_key, - "previousSignature" => previous_signature - } - ) do - tx = %Transaction{ - address: Base.decode16!(address, case: :mixed), - type: decode_type(type), - timestamp: DateTime.from_unix!(timestamp, :millisecond), - data: %TransactionData{ - content: content |> Base.decode16!(case: :mixed), - code: code, - keys: %Keys{ - secret: Base.decode16!(secret, case: :mixed), - authorized_keys: - Enum.reduce(authorized_keys, %{}, fn {public_key, encrypted_secret_key}, acc -> - Map.put( - acc, - Base.decode16!(public_key, case: :mixed), - Base.decode16!(encrypted_secret_key, case: :mixed) - ) - end) - }, - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: - Enum.map(transfers, fn %{"to" => to, "amount" => amount} -> - %Transfer{ - to: Base.decode16!(to, case: :mixed), - amount: amount - } - end) - } - }, - recipients: Enum.map(recipients, &Base.decode16!(&1, case: :mixed)) - }, - previous_public_key: Base.decode16!(previous_public_key, case: :mixed), - previous_signature: Base.decode16!(previous_signature, case: :mixed) - } + def new(conn, params = %{}) do + case decode_pending_transaction(params) do + tx = %Transaction{origin_signature: nil} -> + tx = %{tx | origin_signature: Transaction.serialize(tx) |> Crypto.sign_with_node_key()} + Uniris.send_new_transaction(tx) - tx = %{tx | origin_signature: Transaction.serialize(tx) |> Crypto.sign_with_node_key()} - Uniris.send_new_transaction(tx) + tx = %Transaction{} -> + Uniris.send_new_transaction(tx) + end conn |> put_status(201) |> json(%{status: "ok"}) end - def new( - conn, - _params = %{ - "address" => address, - "type" => type, - "timestamp" => timestamp, - "data" => %{ - "code" => code, - "content" => content, - "keys" => %{ - "secret" => secret, - "authorizedKeys" => authorized_keys - }, - "ledger" => %{ - "uco" => %{ - "transfers" => transfers - } - }, - "recipients" => recipients - }, - "previousPublicKey" => previous_public_key, - "previousSignature" => previous_signature, - "originSignature" => origin_signature - } - ) do - tx = %Transaction{ + defp decode_pending_transaction( + _params = %{ + "address" => address, + "type" => type, + "timestamp" => timestamp, + "data" => %{ + "code" => code, + "content" => content, + "keys" => %{ + "secret" => secret, + "authorizedKeys" => authorized_keys + }, + "ledger" => %{ + "uco" => %{ + "transfers" => uco_transfers + }, + "nft" => %{ + "transfers" => nft_transfers + } + }, + "recipients" => recipients + }, + "previousPublicKey" => previous_public_key, + "previousSignature" => previous_signature, + "originSignature" => origin_signature + } + ) do + %Transaction{ address: Base.decode16!(address, case: :mixed), type: decode_type(type), timestamp: DateTime.from_unix!(timestamp, :millisecond), @@ -125,8 +77,18 @@ defmodule UnirisWeb.API.TransactionController do ledger: %Ledger{ uco: %UCOLedger{ transfers: - Enum.map(transfers, fn %{"to" => to, "amount" => amount} -> - %Transfer{ + Enum.map(uco_transfers, fn %{"to" => to, "amount" => amount} -> + %UCOTransfer{ + to: Base.decode16!(to, case: :mixed), + amount: amount + } + end) + }, + nft: %NFTLedger{ + transfers: + Enum.map(nft_transfers, fn %{"nft" => nft, "to" => to, "amount" => amount} -> + %NFTTransfer{ + nft: Base.decode16!(nft, case: :mixed), to: Base.decode16!(to, case: :mixed), amount: amount } @@ -140,11 +102,16 @@ defmodule UnirisWeb.API.TransactionController do origin_signature: Base.decode16!(origin_signature, case: :mixed) } - Uniris.send_new_transaction(tx) + # if Map.has_key?(params, :origin_signature) do + # origin_signature = + # params + # |> Map.get(:origin_signature) + # |> Base.decode16!(case: :mixed) - conn - |> put_status(201) - |> json(%{status: "ok"}) + # %{tx | origin_signature: origin_signature} + # else + # tx + # end end defp decode_type("identity"), do: :identity @@ -153,6 +120,7 @@ defmodule UnirisWeb.API.TransactionController do defp decode_type("hosting"), do: :hosting defp decode_type("code_proposal"), do: :code_proposal defp decode_type("code_approval"), do: :code_approval + defp decode_type("nft"), do: :nft def last_transaction_content(conn, params = %{"address" => address}) do with {:ok, address} <- Base.decode16(address, case: :mixed), diff --git a/lib/uniris_web/controllers/explorer_controller.ex b/lib/uniris_web/controllers/explorer_controller.ex index 88536fd99..f3ffbeeb4 100644 --- a/lib/uniris_web/controllers/explorer_controller.ex +++ b/lib/uniris_web/controllers/explorer_controller.ex @@ -44,13 +44,13 @@ defmodule UnirisWeb.ExplorerController do def chain(conn, _params = %{"address" => address}) do bin_address = Base.decode16!(address, case: :mixed) chain = Uniris.get_transaction_chain(bin_address) - inputs = Uniris.get_transaction_inputs(bin_address) + %{uco: uco_balance} = Uniris.get_balance(bin_address) render(conn, "chain.html", transaction_chain: chain, address: bin_address, chain_size: Enum.count(chain), - balance: Enum.reduce(inputs, 0.0, &(&2 + &1.amount)) + uco_balance: uco_balance ) end diff --git a/lib/uniris_web/controllers/node_controller.ex b/lib/uniris_web/controllers/node_controller.ex index f447758d7..90e5d77da 100644 --- a/lib/uniris_web/controllers/node_controller.ex +++ b/lib/uniris_web/controllers/node_controller.ex @@ -16,8 +16,8 @@ defmodule UnirisWeb.NodeController do case P2P.get_node_info(pub) do {:ok, node} -> node_address = Crypto.hash(pub) - balance = Uniris.get_balance(node_address) - render(conn, "show.html", node: node, balance: balance) + %{uco: uco_balance} = Uniris.get_balance(node_address) + render(conn, "show.html", node: node, uco_balance: uco_balance) _ -> render(conn, "show.html", node: nil, balance: 0.0) diff --git a/lib/uniris_web/live/top_node_live.ex b/lib/uniris_web/live/top_node_live.ex index 7f883996f..b6876d0d2 100644 --- a/lib/uniris_web/live/top_node_live.ex +++ b/lib/uniris_web/live/top_node_live.ex @@ -1,6 +1,6 @@ defmodule UnirisWeb.TopNodeLive do @moduledoc false - use UnirisWeb, :live_component + use UnirisWeb, :live_view alias Phoenix.View @@ -11,7 +11,7 @@ defmodule UnirisWeb.TopNodeLive do alias UnirisWeb.ExplorerView - def mount(socket) do + def mount(_params, _session, socket) do if connected?(socket) do PubSub.register_to_node_update() end diff --git a/lib/uniris_web/live/top_transactions_live.ex b/lib/uniris_web/live/top_transactions_live.ex index ebf8b8117..a071b24ee 100644 --- a/lib/uniris_web/live/top_transactions_live.ex +++ b/lib/uniris_web/live/top_transactions_live.ex @@ -1,6 +1,6 @@ defmodule UnirisWeb.TopTransactionLive do @moduledoc false - use UnirisWeb, :live_component + use UnirisWeb, :live_view alias Phoenix.View @@ -12,7 +12,7 @@ defmodule UnirisWeb.TopTransactionLive do @nb_transactions 3 - def mount(socket) do + def mount(_params, _session, socket) do if connected?(socket) do PubSub.register_to_new_transaction() end diff --git a/lib/uniris_web/live/transaction_list_live.ex b/lib/uniris_web/live/transaction_list_live.ex index c361c7e90..fbf2f2fee 100644 --- a/lib/uniris_web/live/transaction_list_live.ex +++ b/lib/uniris_web/live/transaction_list_live.ex @@ -99,5 +99,8 @@ defmodule UnirisWeb.TransactionListLive do defp filter_transactions_by_type(transactions, "node_shared_secrets"), do: Stream.filter(transactions, &(&1.type == :node_shared_secrets)) + defp filter_transactions_by_type(transactions, "nft"), + do: Stream.filter(transactions, &(&1.type == :nft)) + defp filter_transactions_by_type(transactions, _), do: transactions end diff --git a/lib/uniris_web/templates/explorer/chain.html.eex b/lib/uniris_web/templates/explorer/chain.html.eex index 66dd5f0af..cad8a5d57 100644 --- a/lib/uniris_web/templates/explorer/chain.html.eex +++ b/lib/uniris_web/templates/explorer/chain.html.eex @@ -78,8 +78,8 @@
-

Balance

-

<%= @balance %> UCO

+

UCO Balance

+

<%= format_float(@uco_balance) %> UCO

@@ -95,7 +95,7 @@ <% end %>
- <%= tx.type %> + <%= format_transaction_type(tx.type) %>
<%= length(tx.validation_stamp.ledger_operations.unspent_outputs) %> Unspent outputs @@ -111,81 +111,4 @@
<% end %>
- <% end %> - - \ No newline at end of file + <% end %> \ No newline at end of file diff --git a/lib/uniris_web/templates/explorer/index.html.leex b/lib/uniris_web/templates/explorer/index.html.leex index 9bd1cbe80..59084c63c 100644 --- a/lib/uniris_web/templates/explorer/index.html.leex +++ b/lib/uniris_web/templates/explorer/index.html.leex @@ -32,7 +32,7 @@
- <%= live_component(@socket, UnirisWeb.TopTransactionLive, []) %> + <%= live_render(@socket, UnirisWeb.TopTransactionLive, id: "top_transactions") %>
@@ -44,7 +44,7 @@
- <%= live_component(@socket, UnirisWeb.TopNodeLive, []) %> + <%= live_render(@socket, UnirisWeb.TopNodeLive, id: "top_nodes") %>
diff --git a/lib/uniris_web/templates/explorer/ko_transaction.html.leex b/lib/uniris_web/templates/explorer/ko_transaction.html.leex index 5b3b5385d..c162cdb1f 100644 --- a/lib/uniris_web/templates/explorer/ko_transaction.html.leex +++ b/lib/uniris_web/templates/explorer/ko_transaction.html.leex @@ -1,14 +1,14 @@ -
-
-
-
-

Transaction invalid

-
-
- <%= Base.encode16(@address) %> -
-
-
-
+ +
+
+
+

Transaction invalid

+
+
+ +
+
+ <%= Base.encode16(@address) %> +
\ No newline at end of file diff --git a/lib/uniris_web/templates/explorer/top_transactions.html.leex b/lib/uniris_web/templates/explorer/top_transactions.html.leex index f21dd0b9e..19f02a089 100644 --- a/lib/uniris_web/templates/explorer/top_transactions.html.leex +++ b/lib/uniris_web/templates/explorer/top_transactions.html.leex @@ -1,4 +1,4 @@ <%= for tx <- @transactions do %> - <%= component "transaction_summary.html", transaction: tx, conn: @socket %> + <%= render "transaction_summary.html", transaction: tx, conn: @socket %>
<% end %> \ No newline at end of file diff --git a/lib/uniris_web/templates/explorer/transaction_details.html.leex b/lib/uniris_web/templates/explorer/transaction_details.html.leex index 2fbd4ffc6..c2e885c6b 100644 --- a/lib/uniris_web/templates/explorer/transaction_details.html.leex +++ b/lib/uniris_web/templates/explorer/transaction_details.html.leex @@ -1,62 +1,34 @@ -
-
-
-

Transaction information

-
-
- <%= link class: "button is-primary is-outlined is-fullwidth", to: Routes.explorer_path(@socket, :chain, address: Base.encode16(@address)) do%> - Explore chain - <% end %> -
-
- -
-
-

Transaction inputs

- <%= for input <- @inputs do %> -
-
- <%= if input.spent? do %> - Spent - <% else %> - Unspent - <% end %> -
-
- <%= link to: Routes.live_path(@socket, UnirisWeb.TransactionDetailsLive, Base.encode16(input.from)) do%> - <%= Base.encode16(input.from) %> - <%= Base.encode16(:binary.part(input.from, 0, 13)) %>... - <% end %> -
-
<%= input.amount %> UCO
-
- <% end %> -
-
- -
+

Transaction - <%= Base.encode16(@address) %>

+
<%= if @transaction == nil do %> -

Transaction does not exists yet

+

Transaction does not exists

<% else %> +
+
+ <%= link class: "button is-primary is-outlined is-fullwidth", to: Routes.live_path(@socket, UnirisWeb.TransactionDetailsLive, Base.encode16(@previous_address)) do%> + Previous transaction + <% end %> +
+
+ <%= link class: "button is-primary is-outlined is-fullwidth", to: Routes.explorer_path(@socket, :chain, address: Base.encode16(@address)) do%> + Explore chain + <% end %> +
+
-

Transaction details

"> -
-
-

Address

- <%= Base.encode16(@address) %> -
-
+
+

Type

<%= format_transaction_type(@transaction.type) %>
@@ -66,7 +38,7 @@
-
+