From ace44a7cdfd7626b1e9ecb15cf217ab12e39a20b Mon Sep 17 00:00:00 2001 From: bchamagne <74045243+bchamagne@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:49:59 +0200 Subject: [PATCH] Smart Contracts playbooks rework (#994) * Rework Smart Contracts playbooks * Adapt playbook since we cannot use datetime with seconds anymore * Update after rebase to match new contract spec --------- Co-authored-by: Neylix --- lib/archethic/contracts.ex | 13 + .../transaction/data/recipient.ex | 4 +- lib/archethic/utils/regression.ex | 3 +- lib/archethic/utils/regression/api.ex | 586 ++++++++++++++++++ .../benchmarks/end_to_end_validation.ex | 37 +- lib/archethic/utils/regression/playbook.ex | 474 +------------- .../regression/playbooks/smart_contract.ex | 171 +++-- .../playbooks/smart_contract/counter.ex | 65 ++ .../playbooks/smart_contract/legacy.ex | 84 +++ .../playbooks/smart_contract/uco_ath.ex | 69 +++ .../utils/regression/playbooks/uco.ex | 72 +-- .../interpreter/action_interpreter_test.exs | 4 +- 12 files changed, 984 insertions(+), 598 deletions(-) create mode 100644 lib/archethic/utils/regression/api.ex create mode 100644 lib/archethic/utils/regression/playbooks/smart_contract/counter.ex create mode 100644 lib/archethic/utils/regression/playbooks/smart_contract/legacy.ex create mode 100644 lib/archethic/utils/regression/playbooks/smart_contract/uco_ath.ex diff --git a/lib/archethic/contracts.ex b/lib/archethic/contracts.ex index 6c86522ec..f6ea8b99d 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -26,6 +26,19 @@ defmodule Archethic.Contracts do @extended_mode? Mix.env() != :prod + @doc """ + Return the minimum trigger interval in milliseconds. + Depends on the env + """ + @spec minimum_trigger_interval(boolean()) :: pos_integer() + def minimum_trigger_interval(extended_mode? \\ @extended_mode?) do + if extended_mode? do + 1_000 + else + 60_000 + end + end + @doc """ Parse a smart contract code and return a contract struct """ diff --git a/lib/archethic/transaction_chain/transaction/data/recipient.ex b/lib/archethic/transaction_chain/transaction/data/recipient.ex index 37d853267..9f56311ad 100644 --- a/lib/archethic/transaction_chain/transaction/data/recipient.ex +++ b/lib/archethic/transaction_chain/transaction/data/recipient.ex @@ -14,8 +14,8 @@ defmodule Archethic.TransactionChain.TransactionData.Recipient do @type t :: %__MODULE__{ address: Crypto.prepended_hash(), - action: String.t(), - args: list(any()) + action: String.t() | nil, + args: list(any()) | nil } @doc """ diff --git a/lib/archethic/utils/regression.ex b/lib/archethic/utils/regression.ex index 489f46652..5e2d7fe1f 100644 --- a/lib/archethic/utils/regression.ex +++ b/lib/archethic/utils/regression.ex @@ -6,13 +6,14 @@ defmodule Archethic.Utils.Regression do alias Archethic.Utils + alias Archethic.Utils.Regression.Playbook.SmartContract alias Archethic.Utils.Regression.Playbook.UCO alias Archethic.Utils.WebClient alias Archethic.Utils.Regression.Benchmark.EndToEndValidation alias Archethic.Utils.Regression.Benchmark.P2PMessage - @playbooks [UCO] + @playbooks [UCO, SmartContract] @benchmarks [P2PMessage, EndToEndValidation] def run_playbooks(nodes, opts \\ []) do diff --git a/lib/archethic/utils/regression/api.ex b/lib/archethic/utils/regression/api.ex new file mode 100644 index 000000000..12c4a97c7 --- /dev/null +++ b/lib/archethic/utils/regression/api.ex @@ -0,0 +1,586 @@ +defmodule Archethic.Utils.Regression.Api do + @moduledoc """ + Collection of functions to work on the Apis + """ + + require Logger + alias Archethic.Crypto + + alias Archethic.Utils.WebSocket.Client, as: WSClient + + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + alias Archethic.TransactionChain.TransactionData.Ledger + alias Archethic.TransactionChain.TransactionData.TokenLedger + alias Archethic.TransactionChain.TransactionData.TokenLedger.Transfer, as: TokenTransfer + alias Archethic.TransactionChain.TransactionData.Ownership + alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.TransactionData.UCOLedger + alias Archethic.TransactionChain.TransactionData.UCOLedger.Transfer, as: UCOTransfer + + alias Archethic.Utils + alias Archethic.Utils.WebClient + + alias Archethic.Bootstrap.NetworkInit + + @genesis_origin_private_key "01019280BDB84B8F8AEDBA205FE3552689964A5626EE2C60AA10E3BF22A91A036009" + |> Base.decode16!() + + @genesis_origin_public_key Application.compile_env!( + :archethic, + [NetworkInit, :genesis_origin_public_keys] + ) + |> Enum.at(0) + + @faucet_seed Application.compile_env(:archethic, [ArchethicWeb.Explorer.FaucetController, :seed]) + + defstruct [ + :host, + :port, + protocol: :http + ] + + @type t() :: %__MODULE__{ + host: String.t(), + port: integer(), + protocol: :http | :https + } + + @doc """ + Send funds to the given seeds + """ + @spec send_funds_to_seeds(%{String.t() => integer()}, t()) :: String.t() + def send_funds_to_seeds(amount_by_seed, endpoint) do + amount_by_address = + amount_by_seed + |> Enum.map(fn {seed, amount} -> + genesis_address = + Crypto.derive_keypair(seed, 0, :ed25519) + |> elem(0) + |> Crypto.derive_address() + + {genesis_address, amount} + end) + |> Enum.into(%{}) + + send_funds_to_addresses(amount_by_address, endpoint) + end + + @doc """ + Send funds to the given hexadecimal addresses + """ + @spec send_funds_to_addresses(%{String.t() => integer()}, t()) :: String.t() + def send_funds_to_addresses(amount_by_address, endpoint) do + transfers = + Enum.map(amount_by_address, fn {address, amount} -> + Logger.debug("FAUCET: sending #{amount} UCOs to '#{Base.encode16(address)}'") + + %UCOTransfer{ + to: address, + amount: Utils.to_bigint(amount) + } + end) + + {:ok, funding_tx_address} = + send_transaction_with_await_replication( + @faucet_seed, + :transfer, + %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: transfers + } + } + }, + endpoint + ) + + Logger.debug("FAUCET: transaction address: #{Base.encode16(funding_tx_address)}") + funding_tx_address + end + + @doc """ + Send transaction without waiting for validation + """ + @spec send_transaction( + String.t(), + atom(), + TransactionData.t(), + t() + ) :: {:ok, String.t()} | {:error, term()} + def send_transaction( + transaction_seed, + tx_type, + transaction_data = %TransactionData{}, + endpoint + ) do + curve = Crypto.default_curve() + chain_length = get_chain_size(transaction_seed, curve, endpoint) + + {previous_public_key, previous_private_key} = + Crypto.derive_keypair(transaction_seed, chain_length, curve) + + {next_public_key, _} = Crypto.derive_keypair(transaction_seed, chain_length + 1, curve) + + genesis_origin_private_key = get_origin_private_key(endpoint) + + tx = + %Transaction{ + address: Crypto.derive_address(next_public_key), + type: tx_type, + data: transaction_data, + previous_public_key: previous_public_key + } + |> Transaction.previous_sign_transaction_with_key(previous_private_key) + |> Transaction.origin_sign_transaction(genesis_origin_private_key) + + true = + Crypto.verify?( + tx.previous_signature, + Transaction.extract_for_previous_signature(tx) |> Transaction.serialize(), + tx.previous_public_key + ) + + case WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.json(&1, "/api/transaction", tx_to_json(tx)), + endpoint.protocol + ) do + {:ok, %{"status" => "pending"}} -> + {:ok, tx.address} + + _ -> + :error + end + end + + @doc """ + Send transaction and wait for validation + """ + @spec send_transaction_with_await_replication( + String.t(), + atom(), + TransactionData.t(), + t(), + Keyword.t() + ) :: {:ok, String.t()} | {:error, term()} + def send_transaction_with_await_replication( + transaction_seed, + tx_type, + transaction_data = %TransactionData{}, + endpoint, + opts \\ [] + ) do + curve = Crypto.default_curve() + chain_length = get_chain_size(transaction_seed, curve, endpoint) + + {previous_public_key, previous_private_key} = + Crypto.derive_keypair(transaction_seed, chain_length, curve) + + {next_public_key, _} = Crypto.derive_keypair(transaction_seed, chain_length + 1, curve) + + genesis_origin_private_key = get_origin_private_key(endpoint) + + tx = + %Transaction{ + address: Crypto.derive_address(next_public_key), + type: tx_type, + data: transaction_data, + previous_public_key: previous_public_key + } + |> Transaction.previous_sign_transaction_with_key(previous_private_key) + |> Transaction.origin_sign_transaction(genesis_origin_private_key) + + true = + Crypto.verify?( + tx.previous_signature, + Transaction.extract_for_previous_signature(tx) |> Transaction.serialize(), + tx.previous_public_key + ) + + replication_attestation = Task.async(fn -> await_replication(tx.address) end) + + case WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.json(&1, "/api/transaction", tx_to_json(tx)), + endpoint.protocol + ) do + {:ok, %{"status" => "pending"}} -> + await_timeout = Keyword.get(opts, :await_timeout, 5000) + + case Task.yield(replication_attestation, await_timeout) || + Task.shutdown(replication_attestation) do + {:ok, :ok} -> + {:ok, tx.address} + + {:ok, {:error, reason}} -> + Logger.error( + "Transaction #{Base.encode16(tx.address)}confirmation fails - #{inspect(reason)}" + ) + + {:error, reason} + + nil -> + Logger.error("Transaction #{Base.encode16(tx.address)} validation timeouts") + {:error, :timeout} + end + + {:ok, %{"status" => "invalid", "errors" => errors}} -> + {:error, errors} + + {:error, reason} -> + Logger.error( + "Transaction #{Base.encode16(tx.address)} submission fails - #{inspect(reason)}" + ) + + {:error, reason} + end + end + + @doc """ + Get the current nonce public key + """ + @spec get_storage_nonce_public_key(t()) :: String.t() + def get_storage_nonce_public_key(endpoint) do + query = ~s|query {sharedSecrets { storageNoncePublicKey}}| + + case WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.query(&1, query), + endpoint.protocol + ) do + {:ok, + %{ + "data" => %{ + "sharedSecrets" => %{"storageNoncePublicKey" => storage_nonce_public_key} + } + }} -> + Base.decode16!(storage_nonce_public_key) + end + end + + @doc """ + Get the transaction chain's size + """ + @spec get_chain_size(String.t(), atom(), t()) :: integer() + def get_chain_size(seed, curve, endpoint) do + genesis_address = + seed + |> Crypto.derive_keypair(0, curve) + |> elem(0) + |> Crypto.derive_address() + + query = + ~s|query {last_transaction(address: "#{Base.encode16(genesis_address)}"){ chainLength }}| + + case WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.query(&1, query), + endpoint.protocol + ) do + {:ok, %{"errors" => [%{"message" => "transaction_not_exists"}]}} -> + 0 + + {:ok, %{"data" => %{"last_transaction" => %{"chainLength" => chain_length}}}} -> + chain_length + + {:error, error_info} -> + raise "chain size failed #{inspect(error_info)}" + end + end + + @doc """ + Get the last transaction of the chain + """ + @spec get_last_transaction(binary(), t()) :: map() + def get_last_transaction(address, endpoint) do + query = ~s""" + query { + lastTransaction(address: "#{Base.encode16(address)}"){ + type + address + data { + content + code + } + validationStamp{ + timestamp + } + } + } + """ + + {:ok, %{"data" => %{"lastTransaction" => transaction}}} = + WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.query(&1, query), + endpoint.protocol + ) + + transaction + end + + @doc """ + Get the UCO balance of the chain + """ + @spec get_uco_balance(binary(), t()) :: integer() + def get_uco_balance(address, endpoint) do + query = ~s|query {lastTransaction(address: "#{Base.encode16(address)}"){ balance { uco }}}| + + case WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.query(&1, query), + endpoint.protocol + ) do + {:ok, %{"data" => %{"lastTransaction" => %{"balance" => %{"uco" => uco}}}}} -> + uco + + {:ok, %{"errors" => [%{"message" => "transaction_not_exists"}]}} -> + balance_query = ~s| query { balance(address: "#{Base.encode16(address)}") { uco } } | + + {:ok, + %{ + "data" => %{ + "balance" => %{ + "uco" => uco + } + } + }} = + WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.query(&1, balance_query), + endpoint.protocol + ) + + uco + end + end + + @doc """ + Get the inputs of transaction + """ + @spec get_inputs(binary(), t()) :: map() + def get_inputs(address, endpoint) do + query = ~s""" + query { + transactionInputs(address: "#{Base.encode16(address)}"){ + amount + type + from + timestamp + } + } + """ + + case WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.query(&1, query), + endpoint.protocol + ) do + {:ok, %{"data" => %{"transactionInputs" => inputs}}} -> + inputs + end + end + + # ------ PRIVATE --------- + + defp await_replication(txn_address) do + query = """ + subscription { + transactionConfirmed(address: "#{Base.encode16(txn_address)}") { + nbConfirmations + } + } + """ + + WSClient.absinthe_sub( + query, + _var = %{}, + _sub_id = Base.encode16(txn_address) + ) + + query = """ + subscription { + transactionError(address: "#{Base.encode16(txn_address)}") { + reason + } + } + """ + + WSClient.absinthe_sub( + query, + _var = %{}, + _sub_id = Base.encode16(txn_address) + ) + + receive do + %{"transactionConfirmed" => %{"nbConfirmations" => 1}} -> + :ok + + %{"transactionError" => %{"reason" => reason}} -> + {:error, reason} + + {:error, reason} -> + {:error, reason} + end + end + + def get_transaction_fee( + transaction_seed, + tx_type, + transaction_data = %TransactionData{}, + endpoint + ) do + curve = Crypto.default_curve() + chain_length = get_chain_size(transaction_seed, curve, endpoint) + + {previous_public_key, previous_private_key} = + Crypto.derive_keypair(transaction_seed, chain_length, curve) + + {next_public_key, _} = Crypto.derive_keypair(transaction_seed, chain_length + 1, curve) + + genesis_origin_private_key = get_origin_private_key(endpoint) + + tx = + %Transaction{ + address: Crypto.derive_address(next_public_key), + type: tx_type, + data: transaction_data, + previous_public_key: previous_public_key + } + |> Transaction.previous_sign_transaction_with_key(previous_private_key) + |> Transaction.origin_sign_transaction(genesis_origin_private_key) + + true = + Crypto.verify?( + tx.previous_signature, + Transaction.extract_for_previous_signature(tx) |> Transaction.serialize(), + tx.previous_public_key + ) + + case WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.json(&1, "/api/transaction_fee", tx_to_json(tx)), + endpoint.protocol + ) do + {:ok, _transaction_fee} = transaction_fee -> + transaction_fee + + error -> + error + end + end + + defp get_origin_private_key(endpoint) do + body = %{ + "origin_public_key" => Base.encode16(@genesis_origin_public_key) + } + + case WebClient.with_connection( + endpoint.host, + endpoint.port, + &WebClient.json(&1, "/api/origin_key", body), + endpoint.protocol + ) do + {:ok, + %{ + "encrypted_origin_private_keys" => encrypted_origin_private_keys, + "encrypted_secret_key" => encrypted_secret_key + }} -> + aes_key = + Base.decode16!(encrypted_secret_key, case: :mixed) + |> Crypto.ec_decrypt!(@genesis_origin_private_key) + + Base.decode16!(encrypted_origin_private_keys, case: :mixed) + |> Crypto.aes_decrypt!(aes_key) + + _ -> + @genesis_origin_private_key + end + end + + defp tx_to_json(%Transaction{ + version: version, + address: address, + type: type, + data: %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{transfers: uco_transfers}, + token: %TokenLedger{transfers: token_transfers} + }, + code: code, + content: content, + recipients: recipients, + ownerships: ownerships + }, + previous_public_key: previous_public_key, + previous_signature: previous_signature, + origin_signature: origin_signature + }) do + %{ + "version" => version, + "address" => Base.encode16(address), + "type" => Atom.to_string(type), + "previousPublicKey" => Base.encode16(previous_public_key), + "previousSignature" => Base.encode16(previous_signature), + "originSignature" => Base.encode16(origin_signature), + "data" => %{ + "ledger" => %{ + "uco" => %{ + "transfers" => + Enum.map(uco_transfers, fn %UCOTransfer{to: to, amount: amount} -> + %{"to" => Base.encode16(to), "amount" => amount} + end) + }, + "token" => %{ + "transfers" => + Enum.map(token_transfers, fn %TokenTransfer{ + to: to, + amount: amount, + token_address: token_address, + token_id: token_id + } -> + %{ + "to" => Base.encode16(to), + "amount" => amount, + "token" => token_address, + "token_id" => token_id + } + end) + } + }, + "code" => code, + "content" => Base.encode16(content), + "recipients" => + Enum.map(recipients, fn %Recipient{address: address, action: action, args: args} -> + %{"address" => Base.encode16(address), "action" => action, "arg" => args} + end), + "ownerships" => + Enum.map(ownerships, fn %Ownership{ + secret: secret, + authorized_keys: authorized_keys + } -> + %{ + "secret" => Base.encode16(secret), + "authorizedKeys" => + Enum.map(authorized_keys, fn {public_key, encrypted_secret_key} -> + %{ + "publicKey" => Base.encode16(public_key), + "encryptedSecretKey" => Base.encode16(encrypted_secret_key) + } + end) + } + end) + } + } + end +end diff --git a/lib/archethic/utils/regression/benchmarks/end_to_end_validation.ex b/lib/archethic/utils/regression/benchmarks/end_to_end_validation.ex index 32ec97dbe..9a4b10ead 100644 --- a/lib/archethic/utils/regression/benchmarks/end_to_end_validation.ex +++ b/lib/archethic/utils/regression/benchmarks/end_to_end_validation.ex @@ -5,8 +5,8 @@ defmodule Archethic.Utils.Regression.Benchmark.EndToEndValidation do alias Archethic.Crypto + alias Archethic.Utils.Regression.Api alias Archethic.Utils.Regression.Benchmark.SeedHolder - alias Archethic.Utils.Regression.Playbook alias Archethic.Utils.Regression.Benchmark alias Archethic.Utils.WebSocket.Client, as: WSClient @@ -19,6 +19,8 @@ defmodule Archethic.Utils.Regression.Benchmark.EndToEndValidation do def plan([host | _nodes], _opts) do port = Application.get_env(:archethic, ArchethicWeb.Endpoint)[:http][:port] + endpoint = %Api{host: host, port: port, protocol: :http} + WSClient.start_link(host: host, port: port) Logger.info("Starting Benchmark: Transactions Per Seconds at host #{host} and port #{port}") @@ -29,34 +31,26 @@ defmodule Archethic.Utils.Regression.Benchmark.EndToEndValidation do {:ok, pid} = SeedHolder.start_link(seeds: seeds) - allocate_funds(SeedHolder.get_seeds(pid), host, port) + amount = 100 + + Api.send_funds_to_seeds( + SeedHolder.get_seeds(pid) + |> Enum.map(fn seed -> {seed, amount} end) + |> Enum.into(%{}), + endpoint + ) { %{ "UCO Transfer single recipient" => fn -> recipient_seed = SeedHolder.get_random_seed(pid) - uco_transfer_single_recipient(pid, host, port, recipient_seed) + uco_transfer_single_recipient(pid, recipient_seed, endpoint) end }, [parallel: 4] } end - defp allocate_funds(seeds, host, port, amount \\ 100) do - recipient_addresses = - Enum.map( - seeds, - fn seed -> - seed - |> Crypto.derive_keypair(0) - |> elem(0) - |> Crypto.derive_address() - end - ) - - Playbook.batch_send_funds_to(recipient_addresses, host, port, amount) - end - defp get_txn_data(receiver_seed) do recipient_address = receiver_seed @@ -78,15 +72,14 @@ defmodule Archethic.Utils.Regression.Benchmark.EndToEndValidation do } end - defp uco_transfer_single_recipient(pid, host, port, recipient_seed) do + defp uco_transfer_single_recipient(pid, recipient_seed, endpoint) do {sender_seed, index} = SeedHolder.pop_seed(pid) - Playbook.send_transaction_with_await_replication( + Api.send_transaction_with_await_replication( sender_seed, :transfer, get_txn_data(recipient_seed), - host, - port + endpoint ) SeedHolder.put_seed(pid, sender_seed, index) diff --git a/lib/archethic/utils/regression/playbook.ex b/lib/archethic/utils/regression/playbook.ex index 9de9b82a3..0e58a6c32 100644 --- a/lib/archethic/utils/regression/playbook.ex +++ b/lib/archethic/utils/regression/playbook.ex @@ -1,482 +1,16 @@ defmodule Archethic.Utils.Regression.Playbook do @moduledoc """ - Playbook is executed on a testnet to verify correctness of the testnet. + Playbook is executed on a testnet/devnet to verify correctness of the network. """ - require Logger - alias Archethic.Crypto - - alias Archethic.Utils.WebSocket.Client, as: WSClient - - alias Archethic.TransactionChain.Transaction - alias Archethic.TransactionChain.TransactionData - alias Archethic.TransactionChain.TransactionData.Recipient - alias Archethic.TransactionChain.TransactionData.Ledger - alias Archethic.TransactionChain.TransactionData.TokenLedger - alias Archethic.TransactionChain.TransactionData.TokenLedger.Transfer, as: TokenTransfer - alias Archethic.TransactionChain.TransactionData.Ownership - alias Archethic.TransactionChain.TransactionData.UCOLedger - alias Archethic.TransactionChain.TransactionData.UCOLedger.Transfer, as: UCOTransfer - - alias Archethic.Utils.WebClient - - alias Archethic.Bootstrap.NetworkInit + @doc """ + Given a list of nodes forming the network and options, play a scenario. + """ @callback play!([String.t()], Keyword.t()) :: :ok - @genesis_origin_private_key "01019280BDB84B8F8AEDBA205FE3552689964A5626EE2C60AA10E3BF22A91A036009" - |> Base.decode16!() - - @genesis_origin_public_key Application.compile_env!( - :archethic, - [NetworkInit, :genesis_origin_public_keys] - ) - |> Enum.at(0) - - @faucet_seed Application.compile_env(:archethic, [ArchethicWeb.Explorer.FaucetController, :seed]) - defmacro __using__(_opts \\ []) do quote do @behaviour Archethic.Utils.Regression.Playbook end end - - def batch_send_funds_to(list_of_recipient_address, host, port, amount \\ 10) do - transfers = - Enum.map(list_of_recipient_address, fn address -> - %UCOTransfer{ - to: address, - amount: amount * 100_000_000 - } - end) - - send_transaction_with_await_replication( - @faucet_seed, - :transfer, - %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: transfers - } - } - }, - host, - port, - :ed25519 - ) - end - - def send_funds_to(recipient_address, host, port, amount \\ 10) do - send_transaction( - @faucet_seed, - :transfer, - %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %UCOTransfer{ - to: recipient_address, - amount: amount * 100_000_000 - } - ] - } - } - }, - host, - port, - :ed25519 - ) - end - - def send_transaction( - transaction_seed, - tx_type, - transaction_data = %TransactionData{}, - host, - port, - curve \\ Crypto.default_curve(), - proto \\ :http - ) do - chain_length = get_chain_size(transaction_seed, curve, host, port, proto) - - {previous_public_key, previous_private_key} = - Crypto.derive_keypair(transaction_seed, chain_length, curve) - - {next_public_key, _} = Crypto.derive_keypair(transaction_seed, chain_length + 1, curve) - - genesis_origin_private_key = get_origin_private_key(host, port, proto) - - tx = - %Transaction{ - address: Crypto.derive_address(next_public_key), - type: tx_type, - data: transaction_data, - previous_public_key: previous_public_key - } - |> Transaction.previous_sign_transaction_with_key(previous_private_key) - |> Transaction.origin_sign_transaction(genesis_origin_private_key) - - true = - Crypto.verify?( - tx.previous_signature, - Transaction.extract_for_previous_signature(tx) |> Transaction.serialize(), - tx.previous_public_key - ) - - case WebClient.with_connection( - host, - port, - &WebClient.json(&1, "/api/transaction", tx_to_json(tx)), - proto - ) do - {:ok, %{"status" => "pending"}} -> - {:ok, tx.address} - - _ -> - :error - end - end - - def get_transaction_fee( - transaction_seed, - tx_type, - transaction_data = %TransactionData{}, - host, - port, - curve \\ Crypto.default_curve(), - proto \\ :http - ) do - chain_length = get_chain_size(transaction_seed, curve, host, port, proto) - - {previous_public_key, previous_private_key} = - Crypto.derive_keypair(transaction_seed, chain_length, curve) - - {next_public_key, _} = Crypto.derive_keypair(transaction_seed, chain_length + 1, curve) - - genesis_origin_private_key = get_origin_private_key(host, port, proto) - - tx = - %Transaction{ - address: Crypto.derive_address(next_public_key), - type: tx_type, - data: transaction_data, - previous_public_key: previous_public_key - } - |> Transaction.previous_sign_transaction_with_key(previous_private_key) - |> Transaction.origin_sign_transaction(genesis_origin_private_key) - - true = - Crypto.verify?( - tx.previous_signature, - Transaction.extract_for_previous_signature(tx) |> Transaction.serialize(), - tx.previous_public_key - ) - - case WebClient.with_connection( - host, - port, - &WebClient.json(&1, "/api/transaction_fee", tx_to_json(tx)), - proto - ) do - {:ok, _transaction_fee} = transaction_fee -> - transaction_fee - - error -> - error - end - end - - defp get_origin_private_key(host, port, proto) do - body = %{ - "origin_public_key" => Base.encode16(@genesis_origin_public_key) - } - - case WebClient.with_connection( - host, - port, - &WebClient.json(&1, "/api/origin_key", body), - proto - ) do - {:ok, - %{ - "encrypted_origin_private_keys" => encrypted_origin_private_keys, - "encrypted_secret_key" => encrypted_secret_key - }} -> - aes_key = - Base.decode16!(encrypted_secret_key, case: :mixed) - |> Crypto.ec_decrypt!(@genesis_origin_private_key) - - Base.decode16!(encrypted_origin_private_keys, case: :mixed) - |> Crypto.aes_decrypt!(aes_key) - - _ -> - @genesis_origin_private_key - end - end - - def send_transaction_with_await_replication( - transaction_seed, - tx_type, - transaction_data = %TransactionData{}, - host, - port, - curve \\ Crypto.default_curve(), - proto \\ :http, - opts \\ [] - ) do - chain_length = get_chain_size(transaction_seed, curve, host, port, proto) - - {previous_public_key, previous_private_key} = - Crypto.derive_keypair(transaction_seed, chain_length, curve) - - {next_public_key, _} = Crypto.derive_keypair(transaction_seed, chain_length + 1, curve) - - genesis_origin_private_key = get_origin_private_key(host, port, proto) - - tx = - %Transaction{ - address: Crypto.derive_address(next_public_key), - type: tx_type, - data: transaction_data, - previous_public_key: previous_public_key - } - |> Transaction.previous_sign_transaction_with_key(previous_private_key) - |> Transaction.origin_sign_transaction(genesis_origin_private_key) - - true = - Crypto.verify?( - tx.previous_signature, - Transaction.extract_for_previous_signature(tx) |> Transaction.serialize(), - tx.previous_public_key - ) - - replication_attestation = Task.async(fn -> await_replication(tx.address) end) - - case WebClient.with_connection( - host, - port, - &WebClient.json(&1, "/api/transaction", tx_to_json(tx)), - proto - ) do - {:ok, %{"status" => "pending"}} -> - await_timeout = Keyword.get(opts, :await_timeout, 5_000) - - case Task.yield(replication_attestation, await_timeout) || - Task.shutdown(replication_attestation) do - {:ok, :ok} -> - {:ok, tx.address} - - {:ok, {:error, reason}} -> - Logger.error( - "Transaction #{Base.encode16(tx.address)}confirmation fails - #{inspect(reason)}" - ) - - {:error, reason} - - nil -> - Logger.error("Transaction #{Base.encode16(tx.address)} validation timeouts") - {:error, :timeout} - end - - {:ok, %{"status" => "invalid", "errors" => errors}} -> - {:error, errors} - - {:error, reason} -> - Logger.error( - "Transaction #{Base.encode16(tx.address)} submission fails - #{inspect(reason)}" - ) - - {:error, reason} - end - end - - defp await_replication(txn_address) do - query = """ - subscription { - transactionConfirmed(address: "#{Base.encode16(txn_address)}") { - nbConfirmations - } - } - """ - - WSClient.absinthe_sub( - query, - _var = %{}, - _sub_id = Base.encode16(txn_address) - ) - - query = """ - subscription { - transactionError(address: "#{Base.encode16(txn_address)}") { - reason - } - } - """ - - WSClient.absinthe_sub( - query, - _var = %{}, - _sub_id = Base.encode16(txn_address) - ) - - receive do - %{"transactionConfirmed" => %{"nbConfirmations" => n}} when n > 0 -> - :ok - - %{"transactionError" => %{"reason" => reason}} -> - {:error, reason} - - {:error, reason} -> - {:error, reason} - - unknown_msg -> - Logger.warn("await_replication received an unknown message: #{inspect(unknown_msg)}") - end - end - - defp tx_to_json(%Transaction{ - version: version, - address: address, - type: type, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{transfers: uco_transfers}, - token: %TokenLedger{transfers: token_transfers} - }, - code: code, - content: content, - recipients: recipients, - ownerships: ownerships - }, - previous_public_key: previous_public_key, - previous_signature: previous_signature, - origin_signature: origin_signature - }) do - %{ - "version" => version, - "address" => Base.encode16(address), - "type" => Atom.to_string(type), - "previousPublicKey" => Base.encode16(previous_public_key), - "previousSignature" => Base.encode16(previous_signature), - "originSignature" => Base.encode16(origin_signature), - "data" => %{ - "ledger" => %{ - "uco" => %{ - "transfers" => - Enum.map(uco_transfers, fn %UCOTransfer{to: to, amount: amount} -> - %{"to" => Base.encode16(to), "amount" => amount} - end) - }, - "token" => %{ - "transfers" => - Enum.map(token_transfers, fn %TokenTransfer{ - to: to, - amount: amount, - token_address: token_address, - token_id: token_id - } -> - %{ - "to" => Base.encode16(to), - "amount" => amount, - "token" => token_address, - "token_id" => token_id - } - end) - } - }, - "code" => code, - "content" => Base.encode16(content), - "recipients" => - case version do - 1 -> - Enum.map(recipients, fn address -> - %{"address" => Base.encode16(address)} - end) - - 2 -> - Enum.map(recipients, fn %Recipient{address: address, action: action, args: args} -> - %{ - "address" => Base.encode16(address), - "action" => action, - "args" => args - } - end) - end, - "ownerships" => - Enum.map(ownerships, fn %Ownership{ - secret: secret, - authorized_keys: authorized_keys - } -> - %{ - "secret" => Base.encode16(secret), - "authorizedKeys" => - Enum.map(authorized_keys, fn {public_key, encrypted_secret_key} -> - %{ - "publicKey" => Base.encode16(public_key), - "encryptedSecretKey" => Base.encode16(encrypted_secret_key) - } - end) - } - end) - } - } - end - - def get_chain_size(seed, curve, host, port, proto \\ :http) do - genesis_address = - seed - |> Crypto.derive_keypair(0, curve) - |> elem(0) - |> Crypto.derive_address() - - query = - ~s|query {last_transaction(address: "#{Base.encode16(genesis_address)}"){ chainLength }}| - - case WebClient.with_connection(host, port, &WebClient.query(&1, query), proto) do - {:ok, %{"errors" => [%{"message" => "transaction_not_exists"}]}} -> - 0 - - {:ok, %{"data" => %{"last_transaction" => %{"chainLength" => chain_length}}}} -> - chain_length - - {:error, error_info} -> - raise "chain size failed #{inspect(error_info)}" - end - end - - def get_uco_balance(address, host, port, proto \\ :http) do - query = ~s|query {lastTransaction(address: "#{Base.encode16(address)}"){ balance { uco }}}| - - case WebClient.with_connection(host, port, &WebClient.query(&1, query), proto) do - {:ok, %{"data" => %{"lastTransaction" => %{"balance" => %{"uco" => uco}}}}} -> - uco - - {:ok, %{"errors" => [%{"message" => "transaction_not_exists"}]}} -> - balance_query = ~s| query { balance(address: "#{Base.encode16(address)}") { uco } } | - - {:ok, - %{ - "data" => %{ - "balance" => %{ - "uco" => uco - } - } - }} = WebClient.with_connection(host, port, &WebClient.query(&1, balance_query), proto) - - uco - end - end - - def storage_nonce_public_key(host, port, proto \\ :http) do - query = ~s|query {sharedSecrets { storageNoncePublicKey}}| - - case WebClient.with_connection(host, port, &WebClient.query(&1, query), proto) do - {:ok, - %{ - "data" => %{ - "sharedSecrets" => %{"storageNoncePublicKey" => storage_nonce_public_key} - } - }} -> - Base.decode16!(storage_nonce_public_key) - end - end end diff --git a/lib/archethic/utils/regression/playbooks/smart_contract.ex b/lib/archethic/utils/regression/playbooks/smart_contract.ex index aca67ff20..ef1b8086c 100644 --- a/lib/archethic/utils/regression/playbooks/smart_contract.ex +++ b/lib/archethic/utils/regression/playbooks/smart_contract.ex @@ -3,100 +3,147 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract do Play and verify smart contracts. """ + use Archethic.Utils.Regression.Playbook + use Retry + alias Archethic.Crypto alias Archethic.TransactionChain.TransactionData alias Archethic.TransactionChain.TransactionData.Ownership + alias Archethic.TransactionChain.TransactionData.Recipient - alias Archethic.Utils.Regression.Playbook - @unit_uco 100_000_000 + alias Archethic.Utils.Regression.Api + alias Archethic.Utils.WebSocket.Client, as: WSClient - require Logger + alias __MODULE__.Counter + alias __MODULE__.Legacy + alias __MODULE__.UcoAth - use Playbook + require Logger def play!(nodes, opts) do + # TODO: add a debug opts (default: false) + # false: no logs + parallel execution + # true: logs + sequential execution + Crypto.Ed25519.LibSodiumPort.start_link() Logger.info("Play smart contract transactions on #{inspect(nodes)} with #{inspect(opts)}") port = Application.get_env(:archethic, ArchethicWeb.Endpoint)[:http][:port] host = :lists.nth(:rand.uniform(length(nodes)), nodes) - run_smart_contracts(host, port) - end + endpoint = %Api{host: host, port: port, protocol: :http} + Logger.info("Using endpoint: #{inspect(endpoint)}") - defp run_smart_contracts(host, port) do - storage_node_public_key = Playbook.storage_nonce_public_key(host, port) + WSClient.start_link(host: host, port: port) + storage_nonce_pubkey = Api.get_storage_nonce_public_key(endpoint) - run_interval_date_trigger(host, port, storage_node_public_key) + Logger.info("============== CONTRACT: COUNTER ==============") + Counter.play(storage_nonce_pubkey, endpoint) + Logger.info("============== CONTRACT: LEGACY ==============") + Legacy.play(storage_nonce_pubkey, endpoint) + Logger.info("============== CONTRACT: UCO ATH ==============") + UcoAth.play(storage_nonce_pubkey, endpoint) end - defp run_interval_date_trigger(host, port, storage_node_public_key) do - recipient_address2 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> - amount_to_send = trunc(0.1 * @unit_uco) + @doc """ + Deploy a smart contract + """ + @spec deploy(String.t(), TransactionData.t(), binary(), Api.t()) :: binary() + def deploy(seed, data, storage_nonce_pubkey, endpoint) do + Logger.debug("DEPLOY: Deploying contract") + + secret_key = :crypto.strong_rand_bytes(32) + + # add the ownerships required for smart contract + data = %TransactionData{ + data + | ownerships: [ + %Ownership{ + secret: Crypto.aes_encrypt(seed, secret_key), + authorized_keys: %{ + storage_nonce_pubkey => Crypto.ec_encrypt(secret_key, storage_nonce_pubkey) + } + } + | data.ownerships + ] + } + + {:ok, address} = + Api.send_transaction_with_await_replication( + seed, + :contract, + data, + endpoint + ) - contract_owner_address = - Crypto.derive_keypair("contract_playbook_seed", 0, :ed25519) - |> elem(0) - |> Crypto.derive_address() + Logger.debug("DEPLOY: Deployed at #{Base.encode16(address)}") - Logger.info( - "Genesis pool allocation owner is sending 10 UCO to #{Base.encode16(contract_owner_address)}" - ) + address + end - {:ok, funding_tx_address} = Playbook.send_funds_to(contract_owner_address, host, port) - Logger.info("Transaction address: #{Base.encode16(funding_tx_address)}") + @doc """ + Trigger a smart contract by sending a transaction from given seed + By passing the [wait: true] flag, it will block until the contract produces a new transaction + """ + @spec trigger(String.t(), binary(), Api.t(), Keyword.t()) :: binary() + def trigger(trigger_seed, contract_address, endpoint, opts \\ []) do + Logger.debug("TRIGGER: Sending trigger transaction") - Process.sleep(1_000) - contract_balance = Playbook.get_uco_balance(contract_owner_address, host, port) - Logger.info("Contract got #{contract_balance} uco") + %{"address" => last_contract_address_hex} = + Api.get_last_transaction(contract_address, endpoint) - secret_key = :crypto.strong_rand_bytes(32) + last_contract_address = Base.decode16!(last_contract_address_hex) - {:ok, contract_tx_address} = - Playbook.send_transaction( - "contract_playbook_seed", + {:ok, trigger_address} = + Api.send_transaction_with_await_replication( + trigger_seed, :transfer, %TransactionData{ - ownerships: [ - %Ownership{ - secret: Crypto.aes_encrypt("contract_playbook_seed", secret_key), - authorized_keys: %{ - storage_node_public_key => Crypto.ec_encrypt(secret_key, storage_node_public_key) - } - } - ], - code: """ - condition inherit: [ - type: transfer, - uco_transfers: %{ "#{Base.encode16(recipient_address2)}" => #{amount_to_send} } - ] - - actions triggered_by: interval, at: "* * * * * *" do - set_type transfer - add_uco_transfer to: "#{Base.encode16(recipient_address2)}", amount: #{amount_to_send} - end - """ + content: Keyword.get(opts, :content, ""), + recipients: [%Recipient{address: contract_address}] }, - host, - port + endpoint ) - Logger.info( - "Deployed smart contract at #{Base.encode16(contract_tx_address)} which sends 0.1 UCO to #{Base.encode16(recipient_address2)} each seconds" - ) + Logger.debug("TRIGGER: transaction sent at #{Base.encode16(trigger_address)}") - balance = - Enum.reduce(0..4, 0, fn _i, _acc -> - Process.sleep(1_000) - balance = Playbook.get_uco_balance(recipient_address2, host, port) - Logger.info("#{Base.encode16(recipient_address2)} received #{balance} UCO") - balance - end) + if Keyword.get(opts, :wait, false) do + # wait until the contract produces a new transaction + :ok = wait_until_new_transaction(last_contract_address, endpoint) + end - # The recipient address should have received 4 times, 0.1 UCO - true = balance == trunc(0.4 * @unit_uco) + trigger_address + end + + def random_address() do + <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + end + + def random_seed() do + :crypto.strong_rand_bytes(10) + end - :ok + defp wait_until_new_transaction(address, endpoint) do + address_hex = Base.encode16(address) + + # retry every 500ms until 20 retries + retry with: constant_backoff(500) |> Stream.take(20) do + %{"address" => last_address_hex} = Api.get_last_transaction(address, endpoint) + + if last_address_hex == address_hex do + :error + else + :ok + end + after + _ -> + Logger.debug("TRIGGER: contract produced a new transaction") + :ok + else + _ -> + Logger.error("TRIGGER: TIMEOUT: contract did not produce a new transaction in time") + :error + end end end diff --git a/lib/archethic/utils/regression/playbooks/smart_contract/counter.ex b/lib/archethic/utils/regression/playbooks/smart_contract/counter.ex new file mode 100644 index 000000000..80d3cd42d --- /dev/null +++ b/lib/archethic/utils/regression/playbooks/smart_contract/counter.ex @@ -0,0 +1,65 @@ +defmodule Archethic.Utils.Regression.Playbook.SmartContract.Counter do + @moduledoc """ + This contract is triggered by transactions + It starts with content=0 and the number will increment for each transaction received + """ + + alias Archethic.TransactionChain.TransactionData + alias Archethic.Utils.Regression.Api + alias Archethic.Utils.Regression.Playbook.SmartContract + + require Logger + + def play(storage_nonce_pubkey, endpoint) do + contract_seed = SmartContract.random_seed() + trigger_seed = SmartContract.random_seed() + + Api.send_funds_to_seeds( + %{ + contract_seed => 10, + trigger_seed => 10 + }, + endpoint + ) + + contract_address = + SmartContract.deploy( + contract_seed, + %TransactionData{ + content: "0", + code: contract_code() + }, + storage_nonce_pubkey, + endpoint + ) + + SmartContract.trigger(trigger_seed, contract_address, endpoint, wait: true) + SmartContract.trigger(trigger_seed, contract_address, endpoint, wait: true) + SmartContract.trigger(trigger_seed, contract_address, endpoint, wait: true) + SmartContract.trigger(trigger_seed, contract_address, endpoint, wait: true) + + last_tx = Api.get_last_transaction(contract_address, endpoint) + + case last_tx["data"]["content"] do + "4" -> + Logger.info("Smart contract 'counter' content has been incremented successfully") + + content -> + Logger.error("Smart contract 'counter' content is not as expected: #{content}") + end + end + + defp contract_code() do + ~s""" + @version 1 + + # GENERATED BY PLAYBOOK + + condition transaction: [] + actions triggered_by: transaction do + count = String.to_number(contract.content) + 1 + Contract.set_content count + end + """ + end +end diff --git a/lib/archethic/utils/regression/playbooks/smart_contract/legacy.ex b/lib/archethic/utils/regression/playbooks/smart_contract/legacy.ex new file mode 100644 index 000000000..40eb0c624 --- /dev/null +++ b/lib/archethic/utils/regression/playbooks/smart_contract/legacy.ex @@ -0,0 +1,84 @@ +defmodule Archethic.Utils.Regression.Playbook.SmartContract.Legacy do + @moduledoc """ + This contract is triggered every minutes(prod) or every seconds(dev) + It should send 0.1 UCO to the recipient chain every tick + """ + + alias Archethic.Contracts + alias Archethic.TransactionChain.TransactionData + alias Archethic.Utils + alias Archethic.Utils.Regression.Api + alias Archethic.Utils.Regression.Playbook.SmartContract + + require Logger + + def play(storage_nonce_pubkey, endpoint) do + trigger_seed = SmartContract.random_seed() + contract_seed = SmartContract.random_seed() + recipient_address = SmartContract.random_address() + amount_to_send = Utils.to_bigint(0.1) + ticks_count = 4 + + Api.send_funds_to_seeds( + %{ + contract_seed => 10, + trigger_seed => 10 + }, + endpoint + ) + + sleep_ms = 200 + ticks_count * Contracts.minimum_trigger_interval() + + contract_address = + SmartContract.deploy( + contract_seed, + %TransactionData{ + code: contract_code(recipient_address, amount_to_send) + }, + storage_nonce_pubkey, + endpoint + ) + + # wait some ticks + Logger.debug("Sleeping for #{ticks_count} ticks (#{div(sleep_ms, 1000)} seconds)") + Process.sleep(sleep_ms) + + balance = Api.get_uco_balance(recipient_address, endpoint) + + SmartContract.trigger(trigger_seed, contract_address, endpoint, content: "CLOSE_CONTRACT") + + # there's a slight change there will be 1 more tick due to playbook code + if balance in [ticks_count * amount_to_send, (1 + ticks_count) * amount_to_send] do + Logger.info( + "Smart contract 'legacy' received #{Utils.from_bigint(balance)} UCOs after #{ticks_count} ticks" + ) + else + Logger.error( + "Smart contract 'legacy' received #{Utils.from_bigint(balance)} UCOs after #{ticks_count} ticks" + ) + end + end + + defp contract_code(address, amount) do + ~s""" + # GENERATED BY PLAYBOOK + + condition inherit: [ + code: true, + uco_transfers: true + ] + + condition transaction: [] + actions triggered_by: transaction do + if transaction.content == "CLOSE_CONTRACT" do + set_code("condition inherit: []") + end + end + + actions triggered_by: interval, at: "* * * * * *" do + set_type transfer + add_uco_transfer to: "#{Base.encode16(address)}", amount: #{amount} + end + """ + end +end diff --git a/lib/archethic/utils/regression/playbooks/smart_contract/uco_ath.ex b/lib/archethic/utils/regression/playbooks/smart_contract/uco_ath.ex new file mode 100644 index 000000000..a62ea5e49 --- /dev/null +++ b/lib/archethic/utils/regression/playbooks/smart_contract/uco_ath.ex @@ -0,0 +1,69 @@ +defmodule Archethic.Utils.Regression.Playbook.SmartContract.UcoAth do + @moduledoc """ + This contract is triggered by oracle transaction + It tracks the "all-time high" of the UCO price in USD. + Credits to @aime-risson + """ + + alias Archethic.TransactionChain.TransactionData + alias Archethic.Utils.Regression.Api + alias Archethic.Utils.Regression.Playbook.SmartContract + + require Logger + + def play(storage_nonce_pubkey, endpoint) do + contract_seed = SmartContract.random_seed() + + Api.send_funds_to_seeds( + %{contract_seed => 10}, + endpoint + ) + + contract_address = + SmartContract.deploy( + contract_seed, + %TransactionData{ + content: Jason.encode!(%{"ucoMaxPrice" => -1}), + code: contract_code() + }, + storage_nonce_pubkey, + endpoint + ) + + # wait for an oracle tx + Logger.info("Sleeping for 1 oracle tick (60 seconds)") + Process.sleep(60_000) + + last_tx = Api.get_last_transaction(contract_address, endpoint) + + %{"ucoMaxPrice" => value} = Jason.decode!(last_tx["data"]["content"]) + + if value > -1 do + Logger.info("Smart contract 'uco ath' content has been updated successfully") + else + Logger.error("Smart contract 'uco ath' content is not as expected: #{value}") + end + end + + defp contract_code() do + ~s""" + @version 1 + + # GENERATED BY PLAYBOOK + + condition oracle: [ + content: Json.path_match?("$.uco.usd") + ] + + actions triggered_by: oracle do + contract_data = Json.parse(contract.content) + uco_price = Json.path_extract(transaction.content, "$.uco.usd") + + if uco_price > contract_data.ucoMaxPrice do + Contract.set_content(Json.to_string(ucoMaxPrice: uco_price)) + Contract.set_code("@version 1\ncondition inherit: []") + end + end + """ + end +end diff --git a/lib/archethic/utils/regression/playbooks/uco.ex b/lib/archethic/utils/regression/playbooks/uco.ex index a986858c8..e787f9122 100644 --- a/lib/archethic/utils/regression/playbooks/uco.ex +++ b/lib/archethic/utils/regression/playbooks/uco.ex @@ -12,43 +12,40 @@ defmodule Archethic.Utils.Regression.Playbook.UCO do alias Archethic.TransactionChain.TransactionData.UCOLedger alias Archethic.TransactionChain.TransactionData.UCOLedger.Transfer, as: UCOTransfer - alias Archethic.Utils.Regression.Playbook + alias Archethic.Utils.Regression.Api + alias Archethic.Utils.WebSocket.Client, as: WSClient @unit_uco 100_000_000 - use Playbook + use Archethic.Utils.Regression.Playbook def play!(nodes, opts) do Logger.info("Play UCO transactions on #{inspect(nodes)} with #{inspect(opts)}") port = Application.get_env(:archethic, ArchethicWeb.Endpoint)[:http][:port] host = :lists.nth(:rand.uniform(length(nodes)), nodes) - run_transfers(host, port) - end - - defp run_transfers(host, port) do - invalid_transfer(host, port) + endpoint = %Api{host: host, port: port, protocol: :http} + WSClient.start_link(host: host, port: port) - single_recipient_transfer(host, port) + run_transfers(endpoint) end - defp single_recipient_transfer(host, port) do - recipient_address = - Crypto.derive_keypair("recipient_1", 0) |> elem(0) |> Crypto.derive_address() - - Logger.info( - "Genesis pool allocation owner is sending 10 UCO to #{Base.encode16(recipient_address)}" - ) - - prev_balance = Playbook.get_uco_balance(recipient_address, host, port) + defp run_transfers(endpoint) do + invalid_transfer(endpoint) - {:ok, address} = Playbook.send_funds_to(recipient_address, host, port) + single_recipient_transfer(endpoint) + end - Logger.info("Transaction #{Base.encode16(address)} submitted") + defp single_recipient_transfer(endpoint) do + recipient_seed = "recipient_1" - Process.sleep(1_000) + recipient_address = + Crypto.derive_keypair(recipient_seed, 0) + |> elem(0) + |> Crypto.derive_address() - # Ensure the recipient got the 10.0 UCO - new_balance = Playbook.get_uco_balance(recipient_address, host, port) + prev_balance = Api.get_uco_balance(recipient_address, endpoint) + Api.send_funds_to_seeds(%{recipient_seed => 10}, endpoint) + new_balance = Api.get_uco_balance(recipient_address, endpoint) true = new_balance - @@ -63,8 +60,8 @@ defmodule Archethic.Utils.Regression.Playbook.UCO do ) {:ok, address} = - Playbook.send_transaction( - "recipient_1", + Api.send_transaction_with_await_replication( + recipient_seed, :transfer, %TransactionData{ ledger: %Ledger{ @@ -78,31 +75,28 @@ defmodule Archethic.Utils.Regression.Playbook.UCO do } } }, - host, - port + endpoint ) Logger.info("Transaction #{Base.encode16(address)} submitted") - Process.sleep(1_000) - # Ensure the second recipient received the 5.0 UCO - 500_000_000 = Playbook.get_uco_balance(new_recipient_address, host, port) + true = 5 * @unit_uco == Api.get_uco_balance(new_recipient_address, endpoint) Logger.info("#{Base.encode16(new_recipient_address)} received 5.0 UCO") # Ensure the first recipient amount have decreased - recipient_balance2 = Playbook.get_uco_balance(recipient_address, host, port) + recipient_balance2 = Api.get_uco_balance(recipient_address, endpoint) # 5.0 - transaction fee - true = recipient_balance2 <= new_balance - 500_000_000 + true = recipient_balance2 <= new_balance - 5 * @unit_uco Logger.info("#{Base.encode16(recipient_address)} now got #{recipient_balance2} UCO") end - defp invalid_transfer(host, port) do + defp invalid_transfer(endpoint) do from_seed = :crypto.strong_rand_bytes(32) recipient_address = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> {:ok, _tx_address} = - Playbook.send_transaction( + Api.send_transaction( from_seed, :transfer, %TransactionData{ @@ -111,19 +105,19 @@ defmodule Archethic.Utils.Regression.Playbook.UCO do transfers: [ %UCOTransfer{ to: recipient_address, - amount: trunc(10 * @unit_uco) + amount: 10 * @unit_uco } ] } } }, - host, - port + endpoint ) - Process.sleep(1_000) - 0 = Playbook.get_uco_balance(recipient_address, host, port) - 0 = Playbook.get_chain_size(from_seed, Crypto.default_curve(), host, port) + Process.sleep(1000) + + 0 = Api.get_uco_balance(recipient_address, endpoint) + 0 = Api.get_chain_size(from_seed, Crypto.default_curve(), endpoint) Logger.info("Transaction with insufficient funds is rejected") end diff --git a/test/archethic/contracts/interpreter/action_interpreter_test.exs b/test/archethic/contracts/interpreter/action_interpreter_test.exs index c3404f25c..6ac93a863 100644 --- a/test/archethic/contracts/interpreter/action_interpreter_test.exs +++ b/test/archethic/contracts/interpreter/action_interpreter_test.exs @@ -798,7 +798,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code = ~S""" actions triggered_by: transaction do - content = "" + content = "你好" if false do content = "hello" @@ -808,7 +808,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do end """ - assert %Transaction{data: %TransactionData{content: ""}} = sanitize_parse_execute(code) + assert %Transaction{data: %TransactionData{content: "你好"}} = sanitize_parse_execute(code) code = ~S""" actions triggered_by: transaction do