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

added get_token_id function in smart contract library #858

Merged
Merged
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
56 changes: 49 additions & 7 deletions lib/archethic/contracts/interpreter/library.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ defmodule Archethic.Contracts.Interpreter.Library do
TransactionChain,
Contracts.ContractConstants,
Contracts.TransactionLookup,
Contracts.Interpreter.Utils
Utils
}

alias Archethic.Contracts.Interpreter.Utils, as: SCUtils

require Logger

@doc """
Match a regex expression

Expand Down Expand Up @@ -141,7 +145,7 @@ defmodule Archethic.Contracts.Interpreter.Library do
:blake2b
end

:crypto.hash(algo, Utils.maybe_decode_hex(content))
:crypto.hash(algo, SCUtils.maybe_decode_hex(content))
|> Base.encode16()
end

Expand Down Expand Up @@ -180,7 +184,7 @@ defmodule Archethic.Contracts.Interpreter.Library do
2
"""
@spec size(binary() | list()) :: non_neg_integer()
def size(binary) when is_binary(binary), do: binary |> Utils.maybe_decode_hex() |> byte_size()
def size(binary) when is_binary(binary), do: binary |> SCUtils.maybe_decode_hex() |> byte_size()
def size(list) when is_list(list), do: length(list)
def size(map) when is_map(map), do: map_size(map)

Expand All @@ -192,7 +196,7 @@ defmodule Archethic.Contracts.Interpreter.Library do
@spec get_calls(binary()) :: list(map())
def get_calls(contract_address) do
contract_address
|> Utils.maybe_decode_hex()
|> SCUtils.maybe_decode_hex()
|> TransactionLookup.list_contract_transactions()
|> Enum.map(fn {address, _, _} ->
# TODO: parallelize
Expand All @@ -206,7 +210,7 @@ defmodule Archethic.Contracts.Interpreter.Library do
"""
@spec get_genesis_public_key(binary()) :: binary()
def get_genesis_public_key(address) do
bin_address = Utils.maybe_decode_hex(address)
bin_address = SCUtils.maybe_decode_hex(address)
nodes = Election.chain_storage_nodes(bin_address, P2P.authorized_and_available_nodes())
{:ok, key} = download_first_public_key(nodes, bin_address)
Base.encode16(key)
Expand All @@ -228,13 +232,51 @@ defmodule Archethic.Contracts.Interpreter.Library do
@spec timestamp() :: non_neg_integer()
def timestamp, do: DateTime.utc_now() |> DateTime.to_unix()

@doc """
Provide a token id which uniquely identify the token base on it's properties and genesis address.
"""
@spec get_token_id(binary()) :: {:error, binary()} | {:ok, binary()}
def get_token_id(address) do
address = SCUtils.get_address(address, :get_token_id)
t1 = Task.async(fn -> Archethic.fetch_genesis_address_remotely(address) end)
t2 = Task.async(fn -> Archethic.search_transaction(address) end)

with {:ok, {:ok, genesis_address}} <- Task.yield(t1),
{:ok, {:ok, tx}} <- Task.yield(t2),
{:ok, %{id: id}} <- Utils.get_token_properties(genesis_address, tx) do
id
else
{:ok, {:error, :network_issue}} ->
{:error, "Network issue"}

{:ok, {:error, :transaction_not_exists}} ->
{:error, "Transaction not exists"}

{:ok, {:error, :transaction_invalid}} ->
{:error, "Transaction invalid"}

{:error, :decode_error} ->
{:error, "Error in decoding transaction"}

{:error, :not_a_token_transaction} ->
{:error, "Transaction is not of type token"}

{:exit, reason} ->
Logger.debug("Task exited with reason #{inspect(reason)}")
{:error, "Task Exited!"}

nil ->
{:error, "Task didn't responded within timeout!"}
end
end

@doc """
Get the genesis address of the chain
"""
@spec get_genesis_address(binary()) ::
binary()
def get_genesis_address(address) do
addr_bin = Utils.maybe_decode_hex(address)
addr_bin = SCUtils.maybe_decode_hex(address)
nodes = Election.chain_storage_nodes(address, P2P.authorized_and_available_nodes())

case TransactionChain.fetch_genesis_address_remotely(addr_bin, nodes) do
Expand All @@ -249,7 +291,7 @@ defmodule Archethic.Contracts.Interpreter.Library do
@spec get_first_transaction_address(address :: binary()) ::
binary()
def get_first_transaction_address(address) do
addr_bin = Utils.maybe_decode_hex(address)
addr_bin = SCUtils.maybe_decode_hex(address)
nodes = Election.chain_storage_nodes(address, P2P.authorized_and_available_nodes())

case TransactionChain.fetch_first_transaction_address_remotely(addr_bin, nodes) do
Expand Down
9 changes: 9 additions & 0 deletions lib/archethic/contracts/interpreter/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@ defmodule Archethic.Contracts.Interpreter.Utils do
{node, acc}
end

# Whitelist the get_token_id/1 function
def prewalk(
node = {{:atom, "get_token_id"}, _, [_address]},
acc = {:ok, %{scope: scope}}
)
when scope != :root do
{node, acc}
end

# Whitelist the timestamp/0 function in condition
def prewalk(node = {{:atom, "timestamp"}, _, _}, acc = {:ok, %{scope: scope}})
when scope != :root do
Expand Down
62 changes: 62 additions & 0 deletions lib/archethic/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ defmodule Archethic.Utils do

alias Archethic.P2P.Node

alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData

alias Archethic.Reward.Scheduler, as: RewardScheduler

import Bitwise
Expand Down Expand Up @@ -913,4 +916,63 @@ defmodule Archethic.Utils do
|> Stream.take_while(&(NaiveDateTime.compare(&1, end_of_month_datetime) in [:lt]))
|> Enum.count()
end

@doc """
get token properties based on the genesis address and the transaction
"""
@spec get_token_properties(binary(), Transaction.t()) ::
{:ok, map()} | {:error, :decode_error} | {:error, :not_a_token_transaction}
def get_token_properties(genesis_address, %Transaction{
data: %TransactionData{
content: content,
ownerships: ownerships
},
type: tx_type
})
when tx_type in [:token, :mint_rewards] do
case Jason.decode(content) do
{:ok, map} ->
result = %{
genesis: genesis_address,
name: Map.get(map, "name", ""),
supply: Map.get(map, "supply"),
symbol: Map.get(map, "symbol", ""),
type: Map.get(map, "type"),
decimals: Map.get(map, "decimals", 8),
properties: Map.get(map, "properties", %{}),
collection: Map.get(map, "collection", []),
ownerships: ownerships
}

token_id = get_token_id(genesis_address, result)

{:ok, Map.put(result, :id, token_id)}

_ ->
{:error, :decode_error}
end
end

def get_token_properties(_, _), do: {:error, :not_a_token_transaction}

defp get_token_id(genesis_address, %{
genesis: genesis_address,
name: name,
symbol: symbol,
decimals: decimals,
properties: properties
}) do
data_to_digest =
%{
genesis_address: Base.encode16(genesis_address),
name: name,
symbol: symbol,
properties: properties,
decimals: decimals
}
|> Jason.encode!()

:crypto.hash(:sha256, data_to_digest)
|> Base.encode16()
end
end
77 changes: 16 additions & 61 deletions lib/archethic_web/graphql_schema/resolver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ defmodule ArchethicWeb.GraphQLSchema.Resolver do

alias Archethic.TransactionChain
alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData
alias Archethic.TransactionChain.TransactionInput

alias Archethic.Mining

alias Archethic.Utils

require Logger

@limit_page 10
Expand Down Expand Up @@ -54,83 +55,37 @@ defmodule ArchethicWeb.GraphQLSchema.Resolver do

def get_token(address) do
t1 = Task.async(fn -> Archethic.fetch_genesis_address_remotely(address) end)
t2 = Task.async(fn -> get_transaction_content(address) end)
t2 = Task.async(fn -> Archethic.search_transaction(address) end)

with {:ok, {:ok, genesis_address}} <- Task.yield(t1),
{:ok,
{:ok,
definition = %{
"ownerships" => ownerships,
"supply" => supply,
"type" => type
}}} <- Task.yield(t2) do
properties = Map.get(definition, "properties", %{})
collection = Map.get(definition, "collection", [])
decimals = Map.get(definition, "decimals", 8)
name = Map.get(definition, "name", "")
symbol = Map.get(definition, "symbol", "")

data_to_digest = %{
genesis_address: Base.encode16(genesis_address),
name: name,
symbol: symbol,
properties: properties,
decimals: decimals
}

token_id = :crypto.hash(:sha256, Jason.encode!(data_to_digest)) |> Base.encode16()

{:ok,
%{
genesis: genesis_address,
name: name,
supply: supply,
symbol: symbol,
type: type,
decimals: decimals,
properties: properties,
collection: collection,
ownerships: ownerships,
id: token_id
}}
{:ok, {:ok, tx}} <- Task.yield(t2),
res = {:ok, _get_token_properties} <- Utils.get_token_properties(genesis_address, tx) do
res
else
{:ok, {:error, :network_issue}} ->
{:error, "Network issue"}

{:ok, {:error, :decode_error}} ->
{:ok, {:error, :transaction_not_exists}} ->
{:error, "Transaction not exists"}

{:ok, {:error, :transaction_invalid}} ->
{:error, "Transaction invalid"}

{:error, :decode_error} ->
{:error, "Error in decoding transaction"}

{:ok, {:error, :transaction_not_found}} ->
{:error, "Transaction does not exist!"}
{:error, :not_a_token_transaction} ->
{:error, "Transaction is not of type token"}

{:exit, reason} ->
Logger.debug("Task exited with reason")
Logger.debug(reason)
Logger.debug("Task exited with reason #{inspect(reason)}")
{:error, "Task Exited!"}

nil ->
{:error, "Task didn't responded within timeout!"}
end
end

defp get_transaction_content(address) do
case Archethic.search_transaction(address) do
{:ok,
%Transaction{data: %TransactionData{content: content, ownerships: ownerships}, type: type}}
when type in [:token, :mint_rewards] ->
case Jason.decode(content) do
{:ok, map} ->
{:ok, map |> Map.put("ownerships", ownerships)}

_ ->
{:error, :decode_error}
end

_ ->
{:error, :transaction_not_found}
end
end

def get_inputs(address, paging_offset \\ 0, limit \\ 0) do
inputs =
address
Expand Down
59 changes: 54 additions & 5 deletions test/archethic/contracts/interpreter/library_test.exs
Original file line number Diff line number Diff line change
@@ -1,17 +1,66 @@
defmodule Archethic.Contracts.Interpreter.LibraryTest do
use ArchethicCase

alias Archethic.{Contracts.Interpreter.Library, P2P, P2P.Node}
alias Archethic.Contracts.Interpreter.Library

alias P2P.Message.{
GetFirstTransactionAddress,
FirstTransactionAddress
}
alias Archethic.P2P.Message.GetFirstTransactionAddress
alias Archethic.P2P.Message.FirstTransactionAddress

alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData

alias Archethic.Utils

alias Archethic.P2P
alias Archethic.P2P.Node

doctest Library

import Mox

describe "get_token_id\1" do
test "should return token_id given the address of the transaction" do
tx_seed = :crypto.strong_rand_bytes(32)

tx =
Transaction.new(
:token,
%TransactionData{
content:
Jason.encode!(%{
supply: 300_000_000,
name: "MyToken",
type: "non-fungible",
symbol: "MTK",
properties: %{
global: "property"
},
collection: [
%{image: "link", value: "link"},
%{image: "link", value: "link"},
%{image: "link", value: "link"}
]
})
},
tx_seed,
0
)

genesis_address = "@Alice1"

MockDB
|> stub(:get_transaction, fn _, _, _ -> {:ok, tx} end)
|> stub(:get_genesis_address, fn _ -> genesis_address end)

{:ok, %{id: token_id}} = Utils.get_token_properties(genesis_address, tx)

assert token_id ==
Library.get_token_id(tx.address)
end
end

import Mox

test "get_first_transaction_address/1" do
P2P.add_and_connect_node(%Node{
ip: {127, 0, 0, 1},
Expand Down