Skip to content

Commit

Permalink
smart contract decimals
Browse files Browse the repository at this point in the history
  • Loading branch information
bchamagne committed Jun 17, 2024
1 parent 6c02574 commit f82f993
Show file tree
Hide file tree
Showing 20 changed files with 408 additions and 130 deletions.
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
2 changes: 1 addition & 1 deletion lib/archethic/contracts/interpreter/action_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,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

0 comments on commit f82f993

Please sign in to comment.