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-14 State UTXO #83

Merged
merged 9 commits into from
Nov 7, 2023
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
30 changes: 30 additions & 0 deletions assets/js/editor/language.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,36 @@ export async function setLanguage(monaco) {
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Returns the map values'
},

// State
{
label: 'State.get/1',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'State.get("${1:key}")',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Return the value stored at given key. Return nil if no value'
},
{
label: 'State.get/2',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'State.get("${1:key}", ${2:default})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Return the value stored at given key. Return default if no value.'
},
{
label: 'State.set/2',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'State.set("${1:key}", ${2:value})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Store the value in the state under given key'
},
{
label: 'State.delete/1',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'State.delete("${1:key}")',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Remove the value stored under key from the state'
},
// Time
{
label: 'Time.now/0',
Expand Down
44 changes: 30 additions & 14 deletions lib/archethic_playground.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ defmodule ArchethicPlayground do
@moduledoc """
Main module to run the functionality needed
"""
alias Archethic.Crypto

alias Archethic.Contracts
alias Archethic.Contracts.Contract
alias Archethic.Contracts.Contract.ActionWithoutTransaction
alias Archethic.Contracts.Contract.ActionWithTransaction
alias Archethic.Contracts.Contract.Failure
alias Archethic.Contracts.Contract.State
alias Archethic.Crypto
alias Archethic.TransactionChain.Transaction

alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput
alias ArchethicPlayground.RecipientForm
alias ArchethicPlayground.Transaction, as: PlaygroundTransaction
alias ArchethicPlayground.TriggerForm
alias ArchethicPlayground.RecipientForm
alias ArchethicPlayground.Utils

require Logger
Expand All @@ -32,7 +37,8 @@ defmodule ArchethicPlayground do
@spec execute_function(
contract_tx :: PlaygroundTransaction.t(),
function_name :: String.t(),
args_values :: list(any())
args_values :: list(any()),
maybe_state_utxo :: nil | UnspentOutput.t()
) ::
{:ok, any()}
| {:error, :function_failure}
Expand All @@ -42,10 +48,11 @@ defmodule ArchethicPlayground do
def execute_function(
contract_tx,
function_name,
args_values
args_values,
maybe_state_utxo
) do
{:ok, contract} = parse(contract_tx)
Contracts.execute_function(contract, function_name, args_values)
Contracts.execute_function(contract, function_name, args_values, maybe_state_utxo)
end

@spec execute(PlaygroundTransaction.t(), TriggerForm.t(), list(Mock.t())) ::
Expand Down Expand Up @@ -98,27 +105,36 @@ defmodule ArchethicPlayground do
ArchethicPlayground.MockFunctions.prepare_mocks(mocks)

with {:ok, contract} <- parse(transaction_contract),
maybe_state_utxo <- State.get_utxo_from_transaction(contract.transaction),
:ok <- check_valid_precondition(trigger, contract, maybe_tx, maybe_recipient, datetime),
{:ok, tx_or_nil} <-
%ActionWithTransaction{next_tx: next_tx, next_state_utxo: next_state_utxo} <-
Contracts.execute_trigger(
trigger,
contract,
maybe_tx,
maybe_recipient,
maybe_state_utxo,
time_now: datetime
),
:ok <- check_valid_postcondition(contract, tx_or_nil, datetime),
tx_or_nil <-
:ok <- check_valid_postcondition(contract, next_tx, datetime),
next_tx <-
PlaygroundTransaction.from_archethic(
tx_or_nil,
next_tx,
next_state_utxo,
transaction_contract.seed,
1 + transaction_contract.index
) do
{:ok, tx_or_nil}
{:ok, next_tx}
else
%ActionWithoutTransaction{} ->
{:ok, nil}

%Failure{user_friendly_error: reason} ->
{:error, reason}

{:error, reason} ->
{:error, reason}
end
catch
{:error, reason} ->
{:error, reason}
end

defp get_time_now(mocks) do
Expand Down
10 changes: 10 additions & 0 deletions lib/archethic_playground/mock_functions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ defmodule ArchethicPlayground.MockFunctions do
get_mocked_value("Chain.get_genesis_address/1", [address])
end

@impl Library.Common.Chain
def get_last_address(address) do
get_mocked_value("Chain.get_last_address/1", [address])
end

@impl Library.Common.Chain
def get_last_transaction(address) do
get_mocked_value("Chain.get_last_transaction/1", [address])
end

@impl Library.Common.Chain
def get_first_transaction_address(address) do
get_mocked_value("Chain.get_first_transaction_address/1", [address])
Expand Down
44 changes: 38 additions & 6 deletions lib/archethic_playground/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule ArchethicPlayground.Transaction do
alias __MODULE__.Ownership

alias Archethic.Crypto
alias Archethic.Contracts.Contract.State
alias Archethic.TransactionChain.Transaction, as: ArchethicTransaction
alias Archethic.TransactionChain.TransactionData
alias Archethic.TransactionChain.TransactionData.Recipient, as: ArchethicRecipient
Expand All @@ -24,7 +25,8 @@ defmodule ArchethicPlayground.Transaction do
token_transfers: list(TokenTransfer.t()),
ownerships: list(Ownership.t()),
seed: String.t(),
index: non_neg_integer()
index: non_neg_integer(),
state: String.t()
}

@derive {Jason.Encoder, except: [:__meta__, :id]}
Expand All @@ -35,6 +37,7 @@ defmodule ArchethicPlayground.Transaction do
field(:validation_timestamp, :string)
field(:seed, :string)
field(:index, :integer)
field(:state, :string)
embeds_many(:recipients, Recipient, on_replace: :delete)
embeds_many(:uco_transfers, UcoTransfer, on_replace: :delete)
embeds_many(:token_transfers, TokenTransfer, on_replace: :delete)
Expand All @@ -53,7 +56,7 @@ defmodule ArchethicPlayground.Transaction do

def changeset(transaction, attrs \\ %{}) do
transaction
|> cast(attrs, [:type, :content, :seed, :index, :code, :validation_timestamp])
|> cast(attrs, [:type, :content, :state, :seed, :index, :code, :validation_timestamp])
|> cast_embed(:uco_transfers)
|> cast_embed(:token_transfers)
|> cast_embed(:recipients)
Expand Down Expand Up @@ -163,6 +166,10 @@ defmodule ArchethicPlayground.Transaction do
filter_code? = Keyword.get(opts, :filter_code, false)

Map.from_struct(t)
|> Map.update(:state, nil, fn
nil -> nil
state -> Jason.decode!(state)
end)
|> Enum.reject(fn {key, value} ->
key in [:__meta__, :seed, :index] || value in [nil, [], ""] ||
(filter_code? && key == :code)
Expand Down Expand Up @@ -248,21 +255,30 @@ defmodule ArchethicPlayground.Transaction do
ArchethicTransaction.ValidationStamp.generate_dummy()
| timestamp: Utils.Date.browser_timestamp_to_datetime(t.validation_timestamp),
ledger_operations: %ArchethicTransaction.ValidationStamp.LedgerOperations{
transaction_movements: ArchethicTransaction.get_movements(signed_tx)
transaction_movements: ArchethicTransaction.get_movements(signed_tx),
unspent_outputs:
case extract_state_utxo(t) do
{:ok, nil} -> []
{:ok, state_utxo} -> [state_utxo]
{:error, :invalid_state} -> []
end
}
}
}
end

def from_archethic(nil, _, _), do: nil

def from_archethic(t = %ArchethicTransaction{}, seed, index) do
def from_archethic(t = %ArchethicTransaction{}, maybe_state_utxo, seed, index) do
%__MODULE__{
seed: seed,
index: index,
type: Atom.to_string(t.type),
code: t.data.code,
content: t.data.content,
state:
case maybe_state_utxo do
nil -> nil
state_utxo -> State.from_utxo(state_utxo) |> Jason.encode!()
end,
validation_timestamp:
if t.validation_stamp != nil do
Utils.Date.datetime_to_browser_timestamp(t.validation_stamp.timestamp)
Expand Down Expand Up @@ -335,4 +351,20 @@ defmodule ArchethicPlayground.Transaction do

defp bin_to_hex(nil), do: nil
defp bin_to_hex(bin), do: Base.encode16(bin)

def extract_state_utxo(tx) do
case tx.state do
nil ->
{:ok, nil}

state_str ->
case Jason.decode(state_str) do
{:ok, map} when is_map(map) ->
State.to_utxo(map)

_ ->
{:error, :invalid_state}
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
module={TransactionFormComponent}
display_mocks={true}
display_code={false}
display_state={true}
global_variable="contract"
parent={@myself}
transaction={@transaction}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
module={TransactionFormComponent}
display_mocks={true}
display_code={true}
display_state={false}
global_variable="transaction"
parent={@myself}
transaction={@transaction}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
<.field field={@form[:index]} type="hidden" />
<.field field={@form[:type]} type="select" options={list_transaction_types()} label="Type*" />

<%= if @display_state do %>
<.field
field={@form[:state]}
type="textarea"
label="State"
help_text="Remainder: a contract is always deployed without state"
/>
<% else %>
<.field field={@form[:state]} type="hidden" label="State" />
<% end %>

<.field field={@form[:content]} type="textarea" label="Content" />

<%= if @display_code do %>
Expand All @@ -24,7 +35,12 @@
<input
type="text"
class="pc-text-input"
value={ArchethicPlayground.Utils.Address.from_seed_index(input_value(f, :seed), input_value(f, :index))}
value={
ArchethicPlayground.Utils.Address.from_seed_index(
input_value(f, :seed),
input_value(f, :index) + 1
)
}
disabled
/>
<div class="pc-form-help-text ">
Expand Down Expand Up @@ -184,17 +200,16 @@
/>

<%= if length(input_value(f, :ownerships)) > 0 do %>
<div class="pc-form-help-text ">
You must encode the secret with an AES key.<br/>
Then you must encrypt the AES key for each public key.

<a href="https://wiki.archethic.net/learn/cryptography/" target="_blank">
<.icon
name={:question_mark_circle}
class="inline text-gray-500 cursor-pointer w-6 h-6"
/>
</a>
</div>
<div class="pc-form-help-text ">
You must encode the secret with an AES key.<br />
Then you must encrypt the AES key for each public key.
<a href="https://wiki.archethic.net/learn/cryptography/" target="_blank">
<.icon
name={:question_mark_circle}
class="inline text-gray-500 cursor-pointer w-6 h-6"
/>
</a>
</div>
<% end %>

<%= for ownership_form <- inputs_for f, :ownerships do %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
module={TransactionFormComponent}
display_mocks={true}
display_code={true}
display_state={false}
parent={@myself}
global_variable="transaction"
transaction={@form[:transaction].value}
Expand Down
6 changes: 5 additions & 1 deletion lib/archethic_playground_web/live/editor_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ defmodule ArchethicPlaygroundWeb.EditorLive do
case ArchethicPlayground.execute_function(
socket.assigns.transaction_contract,
function_name,
args_values
args_values,
case Transaction.extract_state_utxo(socket.assigns.transaction_contract) do
{:ok, maybe_state_utxo} -> maybe_state_utxo
{:error, :invalid_state} -> nil
end
) do
{:ok, result} ->
send(self(), {:console, :success, result})
Expand Down