Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

AEIP-19 token recipients #1214

11 changes: 8 additions & 3 deletions lib/archethic/bootstrap/network_init.ex
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,15 @@ defmodule Archethic.Bootstrap.NetworkInit do
operations =
%LedgerOperations{
fee: Mining.get_transaction_fee(tx, 0.07, timestamp),
transaction_movements: Transaction.get_movements(tx)
transaction_movements: Transaction.get_movements(tx),
tokens_to_mint: LedgerOperations.get_utxos_from_transaction(tx, timestamp)
}
|> LedgerOperations.from_transaction(tx, timestamp)
|> LedgerOperations.consume_inputs(tx.address, unspent_outputs, timestamp)
|> LedgerOperations.consume_inputs(
tx.address,
unspent_outputs,
timestamp
)
|> elem(1)

validation_stamp =
%ValidationStamp{
Expand Down
60 changes: 52 additions & 8 deletions lib/archethic/mining/fee.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ defmodule Archethic.Mining.Fee do
alias Archethic.TransactionChain.TransactionData.UCOLedger

@unit_uco 100_000_000
@token_creation_schema :archethic
|> Application.app_dir("priv/json-schemas/token-core.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@token_resupply_schema :archethic
|> Application.app_dir("priv/json-schemas/token-resupply.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@doc """
Determine the fee to paid for the given transaction
Expand Down Expand Up @@ -97,16 +108,22 @@ defmodule Archethic.Mining.Fee do
|> byte_size()
end

defp get_number_recipients(%Transaction{
data: %TransactionData{
ledger: %Ledger{
uco: %UCOLedger{transfers: uco_transfers},
token: %TokenLedger{transfers: token_transfers}
defp get_number_recipients(
tx = %Transaction{
data: %TransactionData{
ledger: %Ledger{
uco: %UCOLedger{transfers: uco_transfers},
token: %TokenLedger{transfers: token_transfers}
}
}
}
}) do
(uco_transfers ++ token_transfers)
|> Enum.uniq_by(& &1.to)
) do
uco_transfers_addresses = uco_transfers |> Enum.map(& &1.to)
token_transfers_addresses = token_transfers |> Enum.map(& &1.to)
token_recipients_addresses = get_token_recipients(tx) |> Enum.map(& &1["to"])

(uco_transfers_addresses ++ token_transfers_addresses ++ token_recipients_addresses)
|> Enum.uniq()
|> length()
end

Expand Down Expand Up @@ -134,4 +151,31 @@ defmodule Archethic.Mining.Fee do
defp cost_per_recipients(nb_recipients, uco_price_in_usd) do
nb_recipients * (0.1 / uco_price_in_usd)
end

defp get_token_recipients(%Transaction{
type: :token,
data: %TransactionData{content: content}
}) do
case Jason.decode(content) do
{:ok, json} ->
cond do
ExJsonSchema.Validator.valid?(@token_creation_schema, json) ->
get_token_recipients_from_json(json)

ExJsonSchema.Validator.valid?(@token_resupply_schema, json) ->
get_token_recipients_from_json(json)

true ->
[]
end

{:error, _} ->
[]
end
end

defp get_token_recipients(_tx), do: []

defp get_token_recipients_from_json(%{"recipients" => recipients}), do: recipients
defp get_token_recipients_from_json(_json), do: []
end
162 changes: 125 additions & 37 deletions lib/archethic/mining/pending_transaction_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do
alias Archethic.SharedSecrets
alias Archethic.SharedSecrets.NodeRenewal

alias Archethic.TaskSupervisor
alias Archethic.TransactionChain
alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData
Expand Down Expand Up @@ -54,11 +55,11 @@ defmodule Archethic.Mining.PendingTransactionValidation do
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@token_schema :archethic
|> Application.app_dir("priv/json-schemas/token-core.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()
@token_creation_schema :archethic
|> Application.app_dir("priv/json-schemas/token-core.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@token_resupply_schema :archethic
|> Application.app_dir("priv/json-schemas/token-resupply.json")
Expand Down Expand Up @@ -768,22 +769,28 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end

defp verify_token_transaction(tx = %Transaction{data: %TransactionData{content: content}}) do
case Jason.decode(content) do
{:error, _} ->
with {:ok, json_token} <- Jason.decode(content),
:ok <- verify_token_creation(tx, json_token) do
verify_token_recipients(json_token)
else
{:error, %Jason.DecodeError{}} ->
{:error, "Invalid token transaction - invalid JSON"}

{:ok, json_token} ->
case {ExJsonSchema.Validator.validate(@token_schema, json_token),
ExJsonSchema.Validator.validate(@token_resupply_schema, json_token)} do
{:ok, _} ->
verify_token_creation(json_token)
{:error, reason} ->
{:error, reason}
end
end

{_, :ok} ->
verify_token_resupply(tx, json_token)
defp verify_token_creation(tx, json_token) do
cond do
ExJsonSchema.Validator.valid?(@token_creation_schema, json_token) ->
verify_token_creation(json_token)

_ ->
{:error, "Invalid token transaction - neither a token creation nor a token resupply"}
end
ExJsonSchema.Validator.valid?(@token_resupply_schema, json_token) ->
verify_token_resupply(tx, json_token)

true ->
{:error, "Invalid token transaction - neither a token creation nor a token resupply"}
end
end

Expand Down Expand Up @@ -825,52 +832,133 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end

defp verify_token_resupply(tx, %{"token_reference" => token_ref}) do
with {:ok, token_address} <- Base.decode16(token_ref, case: :mixed),
# verify same chain
{:ok, genesis_address} <- fetch_previous_tx_genesis_address(tx),
storage_nodes <-
Election.chain_storage_nodes(token_address, P2P.authorized_and_available_nodes()),
{:ok, ^genesis_address} <-
TransactionChain.fetch_genesis_address(token_address, storage_nodes),
# verify token_reference
{:ok,
%Transaction{
data: %TransactionData{
content: content
}
}} <- TransactionChain.fetch_transaction(token_address, storage_nodes),
# strict because there was a json schema validation before
token_address = Base.decode16!(token_ref, case: :mixed)

storage_nodes =
Election.chain_storage_nodes(token_address, P2P.authorized_and_available_nodes())

# fetch in parallel the data we need
tasks = [
Task.Supervisor.async_nolink(TaskSupervisor, fn ->
fetch_previous_tx_genesis_address(tx)
end),
Task.Supervisor.async_nolink(TaskSupervisor, fn ->
TransactionChain.fetch_genesis_address(token_address, storage_nodes)
end),
Task.Supervisor.async_nolink(TaskSupervisor, fn ->
TransactionChain.fetch_transaction(token_address, storage_nodes)
end)
]

# Shut down the tasks that did not reply nor exit
[tx_genesis_result, ref_genesis_result, ref_tx_result] =
Task.yield_many(tasks)
|> Enum.map(fn {task, res} ->
res || Task.shutdown(task, :brutal_kill)
end)

with {:ok, {:ok, genesis_address}} <- tx_genesis_result,
{:ok, {:ok, ^genesis_address}} <- ref_genesis_result,
{:ok, {:ok, %Transaction{data: %TransactionData{content: content}}}} <- ref_tx_result,
{:ok, reference_json_token} <- Jason.decode(content),
%{"type" => "fungible", "allow_mint" => true} <- reference_json_token do
:ok
else
nil ->
{:error, "Timeout when fetching the reference token or the genesis address"}

{:exit, _} ->
{:error, "Error when fetching the reference token or the genesis address"}

%{"type" => "non-fungible"} ->
{:error, "Invalid token transaction - token_reference must be fungible"}

%{"type" => "fungible"} ->
{:error, "Invalid token transaction - token_reference does not have allow_mint: true"}

{:ok, _} ->
{:ok, {:ok, _}} ->
{:error,
"Invalid token transaction - token_reference is not in the same transaction chain"}

{:error, :transaction_not_exists} ->
{:ok, {:error, :transaction_not_exists}} ->
{:error, "Invalid token transaction - token_reference not found"}

{:error, :invalid_transaction} ->
{:ok, {:error, :invalid_transaction}} ->
{:error, "Invalid token transaction - token_reference is invalid"}

{:error, :network_issue} ->
{:ok, {:error, :network_issue}} ->
{:error, "A network issue was raised, please retry later"}

{:error, %Jason.DecodeError{}} ->
{:error,
"Invalid token transaction - token_reference exists but does not contain a valid JSON"}
end
end

:error ->
{:error, "Invalid token transaction - token_reference is not an hexadecimal"}
defp verify_token_recipients(json_token = %{"recipients" => recipients, "supply" => supply})
when is_list(recipients) do
# resupply token transactions do not have a type, but is applied only to fungible tokens
fungible? = Map.get(json_token, "type", "fungible") == "fungible"

%{res: res} =
Enum.reduce_while(
recipients,
%{sum: 0, token_ids: MapSet.new(), res: :ok},
fn recipient = %{"amount" => amount}, acc = %{sum: sum, token_ids: token_ids} ->
with :ok <- validate_token_recipient_amount(amount, fungible?),
:ok <- validate_token_recipient_total(amount, sum, supply),
:ok <- validate_token_recipient_token_id(recipient, fungible?, token_ids) do
token_id = Map.get(recipient, "token_id", 0)

new_acc =
acc
|> Map.update!(:sum, &(&1 + amount))
|> Map.update!(:token_ids, &MapSet.put(&1, token_id))

{:cont, new_acc}
else
error -> {:halt, Map.put(acc, :res, error)}
end
end
)

res
end

defp verify_token_recipients(_), do: :ok

defp validate_token_recipient_amount(_, true), do: :ok
defp validate_token_recipient_amount(amount, false) when amount == @unit_uco, do: :ok

defp validate_token_recipient_amount(_, false),
do: {:error, "Invalid token transaction - invalid amount in recipients"}

defp validate_token_recipient_total(amount, sum, supply) when sum + amount <= supply, do: :ok

defp validate_token_recipient_total(_, _, _),
do: {:error, "Invalid token transaction - sum of recipients' amounts is bigger than supply"}

defp validate_token_recipient_token_id(%{"token_id" => _}, true, _),
do:
{:error,
"Invalid token transaction - recipient with token_id is now allowed on fungible token"}

defp validate_token_recipient_token_id(_recipient, true, _), do: :ok

defp validate_token_recipient_token_id(%{"token_id" => token_id}, false, token_ids) do
if MapSet.member?(token_ids, token_id) do
{:error,
"Invalid token transaction - recipient must have unique token_id for non fungible token"}
else
:ok
end
end

defp validate_token_recipient_token_id(_, false, _),
do:
{:error, "Invalid token transaction - recipient must have token_id for non fungible token"}

defp valid_collection_id?(collection) do
# If an id is specified in an item of the collection,
# all items must have a different specified id
Expand Down
Loading