Skip to content

Commit

Permalink
integrate nft ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
Samuel Manzanera committed Nov 17, 2020
1 parent 8bef95d commit 877bb9d
Show file tree
Hide file tree
Showing 82 changed files with 1,861 additions and 714 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lib/uniris.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
28 changes: 23 additions & 5 deletions lib/uniris/account.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down
185 changes: 185 additions & 0 deletions lib/uniris/account/mem_tables/nft_ledger.ex
Original file line number Diff line number Diff line change
@@ -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
38 changes: 20 additions & 18 deletions lib/uniris/account/mem_tables/uco_ledger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
{
[
Expand All @@ -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()
Expand All @@ -101,7 +101,8 @@ defmodule Uniris.Account.MemTables.UCOLedger do
[
%UnspentOutput{
from: from,
amount: amount
amount: amount,
type: :UCO
}
| acc
]
Expand All @@ -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")
[]
Expand All @@ -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())
Expand All @@ -168,7 +169,8 @@ defmodule Uniris.Account.MemTables.UCOLedger do
%TransactionInput{
from: from,
amount: amount,
spent?: spent?
spent?: spent?,
type: :UCO
}
end)
end
Expand Down
Loading

0 comments on commit 877bb9d

Please sign in to comment.