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

Smart Contracts: use decimals right after tokenization #1533

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 28 additions & 17 deletions lib/archethic/contracts/constants.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ defmodule Archethic.Contracts.Constants do
alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations

alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.TransactionMovement
alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput

alias Archethic.UTXO

alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput
alias Archethic.Utils
alias Archethic.UTXO

@doc """
Same as from_transaction but remove the contract_seed from ownerships
Expand Down Expand Up @@ -149,35 +148,38 @@ defmodule Archethic.Contracts.Constants do
end
}

if contract_version == 0, do: map, else: cast_transaction_amount_to_float(map)
if contract_version == 0, do: map, else: cast_amounts_to_decimals(map)
end

defp cast_transaction_amount_to_float(transaction) do
defp cast_amounts_to_decimals(transaction) do
transaction
|> Map.update!("uco_transfers", &cast_uco_movements_to_float/1)
|> Map.update!("uco_movements", &cast_uco_movements_to_float/1)
|> Map.update!("token_transfers", &cast_token_movements_to_float/1)
|> Map.update!("token_movements", &cast_token_movements_to_float/1)
|> Map.update!("uco_transfers", &cast_uco_amounts/1)
|> Map.update!("uco_movements", &cast_uco_amounts/1)
|> Map.update!("token_transfers", &cast_token_amounts/1)
|> Map.update!("token_movements", &cast_token_amounts/1)
end

defp cast_uco_movements_to_float(movements) do
defp cast_uco_amounts(movements) do
movements
|> Enum.map(fn {address, amount} ->
{address, Utils.from_bigint(amount)}
{address, amount |> Utils.bigint_to_decimal() |> Utils.maybe_decimal_to_integer()}
end)
|> Enum.into(%{})
end

defp cast_token_movements_to_float(movements) do
defp cast_token_amounts(movements) do
movements
|> Enum.map(fn {address, token_transfer} ->
{address, Enum.map(token_transfer, &convert_token_transfer_amount_to_bigint/1)}
{address, Enum.map(token_transfer, &cast_token_amount/1)}
end)
|> Enum.into(%{})
end

defp convert_token_transfer_amount_to_bigint(token_transfer) do
Map.update!(token_transfer, "amount", &Utils.from_bigint/1)
defp cast_token_amount(token_transfer) do
# FIXME: handles different decimals!
Map.update!(token_transfer, "amount", fn amount ->
amount |> Utils.bigint_to_decimal() |> Utils.maybe_decimal_to_integer()
end)
end

@doc """
Expand All @@ -190,10 +192,19 @@ defmodule Archethic.Contracts.Constants do
tokens =
Enum.reduce(tokens, %{}, fn {{token_address, token_id}, amount}, acc ->
key = %{"token_address" => Base.encode16(token_address), "token_id" => token_id}
Map.put(acc, key, Utils.from_bigint(amount))

# FIXME: handle more decimals
Map.put(
acc,
key,
amount |> Utils.bigint_to_decimal() |> Utils.maybe_decimal_to_integer()
)
end)

balance_constants = %{"uco" => Utils.from_bigint(uco_amount), "tokens" => tokens}
balance_constants = %{
"uco" => uco_amount |> Utils.bigint_to_decimal() |> Utils.maybe_decimal_to_integer(),
"tokens" => tokens
}

Map.put(constants, "balance", balance_constants)
end
Expand Down
48 changes: 43 additions & 5 deletions lib/archethic/contracts/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,16 @@ defmodule Archethic.Contracts.Interpreter do
@spec sanitize_code(binary(), list()) :: {:ok, Macro.t()} | {:error, any()}
def sanitize_code(code, opts \\ []) when is_binary(code) do
ignore_meta? = Keyword.get(opts, :ignore_meta?, false)
check_legacy? = Keyword.get(opts, :check_legacy?, true)

opts = [static_atoms_encoder: &atom_encoder/2]
charlist_code = code |> String.to_charlist()

case :elixir.string_to_tokens(charlist_code, 1, 1, "nofile", opts) do
{:ok, tokens} ->
transform_tokens(tokens, ignore_meta?) |> :elixir.tokens_to_quoted("nofile", opts)
tokens
|> transform_tokens(ignore_meta?, check_legacy? && legacy?(tokens))
|> :elixir.tokens_to_quoted("nofile", opts)

error ->
error
Expand All @@ -80,23 +83,46 @@ defmodule Archethic.Contracts.Interpreter do
_ -> {:error, :invalid_syntax}
end

defp transform_tokens(tokens, ignore_meta?) do
defp transform_tokens(tokens, _ignore_meta?, true), do: tokens

defp transform_tokens(tokens, ignore_meta?, false) do
Enum.map(tokens, fn
# Transform 0x to hex
{:int, {line, column, _}, [?0, ?x | hex]} ->
string_hex = hex |> List.to_string() |> String.upcase()
meta = if ignore_meta?, do: {0, 0, nil}, else: {line, column, nil}

{:bin_string, meta, [string_hex]}
[{:bin_string, meta, [string_hex]}]

# Transform floats to decimal (we use the string representation, not the imprecise float)
{:flt, {line, column, _}, charlist} ->
{line, column} =
if ignore_meta? do
{0, 0}
else
{line, column}
end

{:ok, replacement_tokens} =
:elixir.string_to_tokens(
'Decimal.new("#{charlist}")',
line,
column,
"nofile",
[]
)

replacement_tokens

token ->
if ignore_meta? do
{_line, _colum, last} = elem(token, 1)
token |> Tuple.delete_at(1) |> Tuple.insert_at(1, {0, 0, last})
[token |> Tuple.delete_at(1) |> Tuple.insert_at(1, {0, 0, last})]
else
token
[token]
end
end)
|> List.flatten()
end

@doc """
Expand Down Expand Up @@ -501,4 +527,16 @@ defmodule Archethic.Contracts.Interpreter do
end
end
end

defp legacy?(tokens) do
# if it starts with an @ it's not a legacy contract
# eol = end of line
Enum.reduce_while(tokens, true, fn token, acc ->
case elem(token, 0) do
:eol -> {:cont, acc}
:at_op -> {:halt, false}
_ -> {:halt, true}
end
end)
end
end
5 changes: 4 additions & 1 deletion lib/archethic/contracts/interpreter/action_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
def execute(ast, constants, %Transaction{data: %TransactionData{code: code}}) do
:ok = Macro.validate(ast)

# we need to have a big precision to avoid rounding issue
Decimal.Context.set(%Decimal.Context{Decimal.Context.get() | rounding: :floor, precision: 100})

# initiate a transaction that will be used by the "Contract" module
initial_next_tx = %Transaction{type: :contract, data: %TransactionData{code: code}}

Expand Down Expand Up @@ -114,7 +117,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
{{:atom, "at"}, timestamp}
]
)
when is_number(timestamp) do
when is_integer(timestamp) do
case rem(timestamp, 60) do
0 ->
datetime = DateTime.from_unix!(timestamp)
Expand Down
100 changes: 67 additions & 33 deletions lib/archethic/contracts/interpreter/ast_helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule Archethic.Contracts.Interpreter.ASTHelper do
Helper functions to manipulate AST
"""

alias Archethic.Utils

@doc """
Return wether the given ast is a keyword list.
Remember that we convert all keywords to maps in the prewalk.
Expand Down Expand Up @@ -62,21 +64,59 @@ defmodule Archethic.Contracts.Interpreter.ASTHelper do
def is_map?(_), do: false

@doc """
Return wether the given ast is an integer
Return wether the given ast is a number
Every numbers are automatically converted to Decimal in the tokenization

iex> ast = quote do: Decimal.new(1)
...> ASTHelper.is_number?(ast)
true

iex> ast = quote do: 1
iex> ast = quote do: Decimal.new("1.01")
...> ASTHelper.is_number?(ast)
true

iex> ast = quote do: 1.01
iex> ast = quote do: 100_012_030
...> ASTHelper.is_number?(ast)
true

iex> ast = quote do: []
...> ASTHelper.is_number?(ast)
false
"""
@spec is_number?(Macro.t()) :: boolean()
def is_number?(node) do
is_integer(node) || is_float(node)
def is_number?(num) when is_integer(num), do: true

def is_number?({{:., _, [{:__aliases__, _, [:Decimal]}, :new]}, _, [_]}) do
true
end

def is_number?({:try, _, [[do: {_, _, [op, _, _]}, rescue: _]]}) when op in [:+, :-, :/, :*] do
true
end

def is_number?(_), do: false

@doc """
Extract the value of a Decimal.new()

Used when we pattern match @version and action/condition's datetime
"""
@spec decimal_to_integer(Macro.t()) :: integer() | :error
def decimal_to_integer({{:., _, [{:__aliases__, _, [:Decimal]}, :new]}, _, [value]})
when is_binary(value) do
case Integer.parse(value) do
{integer, ""} -> integer
_ -> :error
end
end

def decimal_to_integer({{:., _, [{:__aliases__, _, [:Decimal]}, :new]}, _, [value]})
when is_integer(value) do
value
end

def decimal_to_integer(_), do: :error

@doc ~S"""
Return wether the given ast is an binary

Expand Down Expand Up @@ -113,7 +153,9 @@ defmodule Archethic.Contracts.Interpreter.ASTHelper do
"""
@spec is_variable_or_function_call?(Macro.t()) :: boolean()
def is_variable_or_function_call?(ast) do
is_variable?(ast) || is_function_call?(ast) || is_block?(ast)
# because numbers are Decimal struct, we need to exclude it
# (because it would be the same as is_function_call?)
not is_number?(ast) && (is_variable?(ast) || is_function_call?(ast) || is_block?(ast))
end

@doc """
Expand Down Expand Up @@ -206,22 +248,27 @@ defmodule Archethic.Contracts.Interpreter.ASTHelper do

## Example

iex> ASTHelper.decimal_arithmetic(:+, 1, 2)
3
iex> ASTHelper.decimal_arithmetic(:+, Decimal.new(1), Decimal.new(2))
...> |> Decimal.eq?(Decimal.new(3)
true

iex> ASTHelper.decimal_arithmetic(:+, 1.0, 2)
3.0
iex> ASTHelper.decimal_arithmetic(:+, Decimal.new("1.0"), Decimal.new(2))
...> |> Decimal.eq?(Decimal.new(3)
true

iex> ASTHelper.decimal_arithmetic(:+, 1, 2.2)
3.2
iex> ASTHelper.decimal_arithmetic(:+, Decimal.new(1), Decimal.new("2.2"))
...> |> Decimal.eq?(Decimal.new("3.2")
true

iex> ASTHelper.decimal_arithmetic(:/, 1, 2)
0.5
iex> ASTHelper.decimal_arithmetic(:/, Decimal.new(1), Decimal.new(2))
...> |> Decimal.eq?(Decimal.new("0.5")
true

iex> ASTHelper.decimal_arithmetic(:*, 3, 4)
12
iex> ASTHelper.decimal_arithmetic(:*, Decimal.new(3), Decimal.new(4))
...> |> Decimal.eq?(Decimal.new(12)
true
"""
@spec decimal_arithmetic(Macro.t(), number(), number()) :: float()
@spec decimal_arithmetic(Macro.t(), Decimal.t(), Decimal.t()) :: Decimal.t()
def decimal_arithmetic(ast, lhs, rhs) do
operation =
case ast do
Expand All @@ -231,22 +278,9 @@ defmodule Archethic.Contracts.Interpreter.ASTHelper do
:- -> &Decimal.sub/2
end

# If both operand are integer we return an integer if possible
# otherwise a float is returned

if is_integer(lhs) and is_integer(rhs) do
lhs = Decimal.new(lhs)
rhs = Decimal.new(rhs)

res = operation.(lhs, rhs) |> decimal_round()
if Decimal.integer?(res), do: Decimal.to_integer(res), else: Decimal.to_float(res)
else
# the `0.0 + x` is used to cast integers to floats
lhs = Decimal.from_float(0.0 + lhs)
rhs = Decimal.from_float(0.0 + rhs)

operation.(lhs, rhs) |> decimal_round() |> Decimal.to_float()
end
operation.(lhs, rhs)
|> decimal_round()
|> Utils.maybe_decimal_to_integer()
end

defp decimal_round(dec_num) do
Expand Down
Loading
Loading