From f0bfc323e664223aab0d8b8e018857527308930f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rold=C3=A1n?= <62166813+Odraxs@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:10:43 -0500 Subject: [PATCH 1/6] Test create and upload contract functionality and update docs (#322) Co-authored-by: Edwin Steven Guayacan --- docs/examples/soroban/create_contract.md | 62 ++++++++++++------- docs/examples/soroban/upload_contract_code.md | 18 ++---- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/docs/examples/soroban/create_contract.md b/docs/examples/soroban/create_contract.md index 8aa7cf3e..8330b293 100644 --- a/docs/examples/soroban/create_contract.md +++ b/docs/examples/soroban/create_contract.md @@ -1,4 +1,5 @@ # Create Contract + > **Warning** > Please note that Soroban is still under development, so breaking changes may occur. @@ -13,26 +14,36 @@ There are two types of contract creations: alias Stellar.TxBuild.{ Account, BaseFee, - InvokeHostFunction, + ContractExecutable, + ContractIDPreimage, + ContractIDPreimageFromAddress, + CreateContractArgs, HostFunction, - HostFunctionArgs, + InvokeHostFunction, SequenceNumber, - Signature + Signature, + SCAddress } alias Stellar.Horizon.Accounts -wasm_id = <> +wasm_ref = <> +address = SCAddress.new("GDEU46HFMHBHCSFA3K336I3MJSBZCWVI3LUGSNL6AF2BW2Q2XR7NNAPM") +salt = :crypto.strong_rand_bytes(32) + +address_preimage = ContractIDPreimageFromAddress.new(address: address, salt: salt) +contract_id_preimage = ContractIDPreimage.new(from_address: address_preimage) +contract_executable = ContractExecutable.new(wasm_ref: wasm_ref) -function_args = - HostFunctionArgs.new( - type: :create, - wasm_id: wasm_id +create_contract_args = + CreateContractArgs.new( + contract_id_preimage: contract_id_preimage, + contract_executable: contract_executable ) -function = HostFunction.new(args: function_args) +host_function = HostFunction.new(create_contract: create_contract_args) -invoke_host_function_op = InvokeHostFunction.new(functions: [function]) +invoke_host_function_op = InvokeHostFunction.new(host_function: host_function) keypair = {public_key, _secret} = @@ -54,9 +65,9 @@ source_account # Simulate Transaction soroban_data = - "AAAAAQAAAAdw47HdjY6bhECndnofWxQ6D1mDQaYxvykdKi4f7bz4ygAAAAEAAAAGSCB+H17w48TbOf0PtCgDnGu0zOOzuVjMYSzmMYZOD0kAAAAUAAFCiwAAQ1QAAACAAAAAqAAAAAAAAAAhAAAAAA==" + "AAAAAAAAAAEAAAAHuoVwkiq7sFT5+6wPecWIC3zW3SXzDactjjMN9VUNzQIAAAAAAAAAAQAAAAYAAAABet+3VCiKSYoZDd/Ce32Dtp9tYwNFc64V/QfdZUJm4boAAAAUAAAAAQAAAAAAAX/UAAACyAAAAKQAAADYAAAAAAAAACs=" -min_resource_fee = 54439 +min_resource_fee = 38_733 fee = BaseFee.new(min_resource_fee + 100) # Use the XDR generated here to send it to the futurenet @@ -78,9 +89,11 @@ alias Stellar.TxBuild.{ Account, Asset, BaseFee, - InvokeHostFunction, + CreateContractArgs, + ContractIDPreimage, + ContractExecutable, HostFunction, - HostFunctionArgs, + InvokeHostFunction, SequenceNumber, Signature } @@ -91,15 +104,18 @@ keypair = {public_key, _secret} = Stellar.KeyPair.from_secret_seed("SCA...3EK") asset = Asset.new(code: :ABC, issuer: public_key) -function_args = - HostFunctionArgs.new( - type: :create, - asset: asset +contract_id_preimage = ContractIDPreimage.new(from_asset: asset) + contract_executable = ContractExecutable.new(:token) + +create_contract_args = + CreateContractArgs.new( + contract_id_preimage: contract_id_preimage, + contract_executable: contract_executable ) -function = HostFunction.new(args: function_args) +host_function = HostFunction.new(create_contract: create_contract_args) -invoke_host_function_op = InvokeHostFunction.new(functions: [function]) +invoke_host_function_op = InvokeHostFunction.new(host_function: host_function) source_account = Account.new(public_key) @@ -115,9 +131,9 @@ source_account |> Stellar.TxBuild.envelope() soroban_data = - "AAAAAAAAAAQAAAAGy7BJuM4i1Q0rEQ2fd0X9qJqvJugM2lEpbY21qRJAzmsAAAAPAAAACE1FVEFEQVRBAAAABsuwSbjOItUNKxENn3dF/aiaryboDNpRKW2NtakSQM5rAAAAEAAAAAEAAAABAAAADwAAAAVBZG1pbgAAAAAAAAbLsEm4ziLVDSsRDZ93Rf2omq8m6AzaUSltjbWpEkDOawAAABAAAAABAAAAAQAAAA8AAAAJQXNzZXRJbmZvAAAAAAAABsuwSbjOItUNKxENn3dF/aiaryboDNpRKW2NtakSQM5rAAAAFAAD2ZUAAADgAAADFAAAA/QAAAAAAAAAxgAAAAA=" + "AAAAAAAAAAAAAAABAAAABgAAAAH3mbzcS3de+8X2xbTs9y5hcKA2c5Nvip1EIroM6x+eqAAAABQAAAABAAAAAAAEIzgAAAA0AAACGAAAA0gAAAAAAAAApQ==" -min_resource_fee = 12_7568 +min_resource_fee = 36_155 fee = BaseFee.new(min_resource_fee + 100) # Use the XDR generated here to send it to the futurenet @@ -129,4 +145,4 @@ source_account |> Stellar.TxBuild.sign(signature) |> Stellar.TxBuild.envelope() -``` \ No newline at end of file +``` diff --git a/docs/examples/soroban/upload_contract_code.md b/docs/examples/soroban/upload_contract_code.md index 188a14c2..95de3b1b 100644 --- a/docs/examples/soroban/upload_contract_code.md +++ b/docs/examples/soroban/upload_contract_code.md @@ -1,4 +1,5 @@ # Upload Contract Code (WASM) + > **Warning** > Please note that Soroban is still under development, so breaking changes may occur. @@ -11,7 +12,6 @@ alias Stellar.TxBuild.{ BaseFee, InvokeHostFunction, HostFunction, - HostFunctionArgs, SequenceNumber, Signature } @@ -22,15 +22,9 @@ alias Stellar.KeyPair # read file {:ok, code} = File.read("file_path/file.wasm") -function_args = - HostFunctionArgs.new( - type: :upload, - code: code - ) - -function = HostFunction.new(args: function_args) +host_function = HostFunction.new(upload_contract_wasm: code) -invoke_host_function_op = InvokeHostFunction.new(functions: [function]) +invoke_host_function_op = InvokeHostFunction.new(host_function: host_function) keypair = {public_key, _secret} = @@ -51,9 +45,9 @@ source_account # Simulate Transaction soroban_data = - "AAAAAQAAAAe7u0W71jGn1HRVU31RHkxALif4ynfRG6iiRh4VcL5wGgAAAAAAAUBLAAACSAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAEAAAAHuoVwkiq7sFT5+6wPecWIC3zW3SXzDactjjMN9VUNzQIAAAAAAAAAAAABSTcAAAKUAAAAAAAAAAAAAAAAAAAAAA==" -min_resource_fee = 8552 +min_resource_fee = 8800 fee = BaseFee.new(min_resource_fee + 100) # Use the XDR generated here to send it to the futurenet @@ -65,4 +59,4 @@ source_account |> Stellar.TxBuild.sign(signature) |> Stellar.TxBuild.envelope() -``` \ No newline at end of file +``` From b5a436a4b956ead84aa843748727aecfde707aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rold=C3=A1n?= <62166813+Odraxs@users.noreply.github.com> Date: Tue, 1 Aug 2023 09:26:36 -0500 Subject: [PATCH 2/6] Support authorized invocation with different accounts (#321) * Add sign functions for contract invoke function op's * Documentation update * Add minor changes * Add requested changes --------- Co-authored-by: Edwin Steven Guayacan --- .../soroban/invoke_contract_functions.md | 101 +++++----- lib/tx_build/soroban_authorization_entry.ex | 180 +++++++++++++++++- lib/tx_build/variable_opaque.ex | 25 --- test/support/xdr_fixtures.ex | 19 ++ test/tx_build/invoke_host_function_test.exs | 21 +- .../soroban_authorization_entry_test.exs | 112 ++++++++++- 6 files changed, 368 insertions(+), 90 deletions(-) delete mode 100644 lib/tx_build/variable_opaque.ex diff --git a/docs/examples/soroban/invoke_contract_functions.md b/docs/examples/soroban/invoke_contract_functions.md index 5c02ce9f..1072eacc 100644 --- a/docs/examples/soroban/invoke_contract_functions.md +++ b/docs/examples/soroban/invoke_contract_functions.md @@ -1,4 +1,5 @@ # Invoke Contract Function + > **Warning** > Please note that Soroban is still under development, so breaking changes may occur. @@ -9,6 +10,9 @@ There are three ways to perform a contract function invocation: ### Without Contract Authorization +> **Note** +> Used to invoke functions that don't require any authorization. + ```elixir alias Stellar.TxBuild.{ Account, @@ -66,6 +70,9 @@ source_account ### With Authorization +> **Note** +> Used when the tx submitter and the invoker are the same. + ```elixir alias Stellar.TxBuild.{ InvokeHostFunction, @@ -133,19 +140,18 @@ source_account |> Stellar.TxBuild.envelope() ``` -### With Stellar Account Authorization (WIP: Preview 10 support) +### With Stellar Account Authorization + +> **Note** +> Used when the tx submitter is different from the invoker. ```elixir alias StellarBase.XDR.{SorobanResources, SorobanTransactionData, UInt32} alias Stellar.TxBuild.{ - ContractAuth, - AddressWithNonce, - AuthorizedInvocation, BaseFee, InvokeHostFunction, HostFunction, - HostFunctionArgs, SCVal, SCAddress, SequenceNumber, @@ -155,59 +161,42 @@ alias Stellar.TxBuild.{ alias Stellar.Horizon.Accounts alias Stellar.KeyPair -contract_id = "8367a1324fdbb56d41e6a6cea2364e389e9f4e17d3ebea810d7bdeca663c2cd5" -function_name = "inc" +contract_address = + "CAMGSYINVVL6WP3Q5WPNL7FS4GZP37TWV7MKIRQF5QMYLK3N2SW4P3RC" + |> SCAddress.new() + |> (&SCVal.new(address: &1)).() +function_name = SCVal.new(symbol: "inc") ## invoker -{public_key, secret_key} = +{invoker_public_key, invoker_secret_key} = KeyPair.from_secret_seed("SCAVFA3PI3MJLTQNMXOUNBSEUOSY66YMG3T2KCQKLQBENNVLVKNPV3EK") ## submitter -keypair2 = - {public_key_2, _secret_key_2} = +submitter_keypair = + {submitter_public_key, _submitter_secret_key} = KeyPair.from_secret_seed("SDRD4CSRGPWUIPRDS5O3CJBNJME5XVGWNI677MZDD4OD2ZL2R6K5IQ24") -address_type = SCAddress.new(public_key) +address_type = SCAddress.new(invoker_public_key) address = SCVal.new(address: address_type) -args = [address, SCVal.new(u128: %{hi: 0, lo: 2})] - -function_args = - HostFunctionArgs.new( - type: :invoke, - contract_id: contract_id, - function_name: function_name, - args: args - ) - -auth_invocation = - AuthorizedInvocation.new( - contract_id: contract_id, - function_name: function_name, - args: args, - sub_invocations: [] - ) - -# Nonce increment by 1 each successfully contract call -address_with_nonce = AddressWithNonce.new(address: address_type, nonce: 0) - -contract_auth = - ContractAuth.new( - address_with_nonce: address_with_nonce, - authorized_invocation: auth_invocation - ) - |> ContractAuth.sign(secret_key) - -function = HostFunction.new(args: function_args, auth: [contract_auth]) - -invoke_host_function_op = - InvokeHostFunction.new(functions: [function], source_account: public_key_2) - -source_account = Stellar.TxBuild.Account.new(public_key_2) -{:ok, seq_num} = Accounts.fetch_next_sequence_number(public_key) + +args = + SCVec.new([ + contract_address, + function_name, + address, + SCVal.new(u128: %{hi: 0, lo: 2}) + ]) + +host_function = HostFunction.new(invoke_contract: args) + +invoke_host_function_op = InvokeHostFunction.new(host_function: host_function) + +source_account = Stellar.TxBuild.Account.new(submitter_public_key) +{:ok, seq_num} = Accounts.fetch_next_sequence_number(submitter_public_key) sequence_number = SequenceNumber.new(seq_num) -signature = Stellar.TxBuild.Signature.new(keypair2) +signature = Stellar.TxBuild.Signature.new(submitter_keypair) -# Use this XDR to simulate the transaction and get the soroban_data and min_resource_fee +# Use this XDR to simulate the transaction and get the soroban_data, the invoke_host_function auth and the min_resource_fee source_account |> Stellar.TxBuild.new(sequence_number: sequence_number) |> Stellar.TxBuild.add_operation(invoke_host_function_op) @@ -218,19 +207,27 @@ source_account resources: %SorobanResources{instructions: %UInt32{datum: datum}} = resources } = soroban_data, ""} = - "AAAAAwAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftYAAAAGg2ehMk/btW1B5qbOojZOOJ6fThfT6+qBDXveymY8LNUAAAAUAAAAB0YZgeyJouAsCYZs6da/3e+lEoTyazuT0hzldmfbT2xPAAAAAgAAAAaDZ6EyT9u1bUHmps6iNk44np9OF9Pr6oENe97KZjws1QAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAATAAAAAAAAAADJTnjlYcJxSKDat78jbEyDkVqo2uhpNX4BdBtqGrx+1gAAAAaDZ6EyT9u1bUHmps6iNk44np9OF9Pr6oENe97KZjws1QAAABUAAAAAAAAAAMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WABQR1AAAFLAAAAGoAAACZAAAAAAAAAB4AAAAAA==" + "AAAAAAAAAAIAAAAAAAAAAMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WAAAAB5g18rNSrgYpg/O7tgIlBv42+QqjpEFv6gEqW+oDFUZbAAAAAAAAAAIAAAAGAAAAAAAAAADJTnjlYcJxSKDat78jbEyDkVqo2uhpNX4BdBtqGrx+1gAAABU69WqNb/7SRQAAAAAAAAAAAAAABgAAAAEYaWENrVfrP3DtntX8suGy/f52r9ikRgXsGYWrbdStxwAAABQAAAABAAAAAAA4FMMAABWQAAABmAAAA/AAAAAAAAAAxQ==" |> Base.decode64!() |> SorobanTransactionData.decode_xdr!() +# Use the Soroban RPC `getLatestLedger` endpoint to obtain this number. +# This number needs to be in the same ledger when submitting the transaction, otherwise the function invocation will fail. +latest_ledger = 164_265 + +auth_xdr = "AAAAAQAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftY69WqNb/7SRQAAAAAAAAAAAAAAAAAAAAEYaWENrVfrP3DtntX8suGy/f52r9ikRgXsGYWrbdStxwAAAANpbmMAAAAAAgAAABIAAAAAAAAAAMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WAAAACQAAAAAAAAAAAAAAAAAAAAIAAAAA" + +auth = SorobanAuthorizationEntry.sign_xdr(auth_xdr, invoker_secret_key, latest_ledger) +invoke_host_function_op = InvokeHostFunction.set_auth(invoke_host_function_op, [auth]) + # Needed calculations since simulate_transaction returns soroban_data # with wrong calculated instructions value because there are two signers new_instructions = UInt32.new(datum + round(datum * 0.25)) new_resources = %{resources | instructions: new_instructions} soroban_data = %{soroban_data | resources: new_resources} -# Arbitrary additional fee(10_000) -min_resource_fee = 97_397 + 10_000 -fee = BaseFee.new(min_resource_fee + 100) +# `round(min_resource_fee*0.1)` is needed since the cost of the transaction will increase because there are two signers. +fee = BaseFee.new(min_resource_fee + round(min_resource_fee*0.1) +100) # Use the XDR generated here to send it to the futurenet source_account diff --git a/lib/tx_build/soroban_authorization_entry.ex b/lib/tx_build/soroban_authorization_entry.ex index 93b4eec6..4810bb76 100644 --- a/lib/tx_build/soroban_authorization_entry.ex +++ b/lib/tx_build/soroban_authorization_entry.ex @@ -3,17 +3,36 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntry do `SorobanAuthorizationEntry` struct definition. """ + alias StellarBase.XDR.HashIDPreimage, as: HashIDPreimageXDR + + alias StellarBase.XDR.HashIDPreimageSorobanAuthorization, + as: HashIDPreimageSorobanAuthorizationXDR + + alias StellarBase.XDR.SCVec, as: SCVecXDR + alias StellarBase.XDR.SorobanAddressCredentials, as: SorobanAddressCredentialsXDR + alias StellarBase.XDR.SorobanAuthorizedInvocation, as: SorobanAuthorizedInvocationXDR + alias StellarBase.XDR.{EnvelopeType, Hash, Int64, SorobanAuthorizationEntry, UInt32} + alias Stellar.{KeyPair, Network} + alias Stellar.TxBuild.{ + HashIDPreimage, + HashIDPreimageSorobanAuthorization, + SCMapEntry, + SCVal, + SCVec, + SorobanAddressCredentials, SorobanCredentials, SorobanAuthorizedInvocation } - alias StellarBase.XDR.SorobanAuthorizationEntry - @behaviour Stellar.TxBuild.XDR @type error :: {:error, atom()} @type validation :: {:ok, any()} | error() + @type base_64 :: String.t() + @type secret_key :: String.t() + @type sign_authorization :: String.t() + @type latest_ledger :: non_neg_integer() @type credentials :: SorobanCredentials.t() @type root_invocation :: SorobanAuthorizedInvocation.t() @@ -56,6 +75,119 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntry do def to_xdr(_struct), do: {:error, :invalid_struct} + @spec sign(credentials :: t(), secret_key :: secret_key()) :: t() | error() + def sign( + %__MODULE__{ + credentials: %SorobanCredentials{ + value: + %SorobanAddressCredentials{ + nonce: nonce, + signature_expiration_ledger: signature_expiration_ledger, + signature_args: signature_args + } = soroban_address_credentials + }, + root_invocation: root_invocation + } = credentials, + secret_key + ) + when is_binary(secret_key) do + {public_key, _secret_key} = KeyPair.from_secret_seed(secret_key) + raw_public_key = KeyPair.raw_public_key(public_key) + network_id = network_id_xdr() + + signature = + [ + network_id: network_id, + nonce: nonce, + signature_expiration_ledger: signature_expiration_ledger + 3, + invocation: root_invocation + ] + |> HashIDPreimageSorobanAuthorization.new() + |> (&HashIDPreimage.new(soroban_auth: &1)).() + |> HashIDPreimage.to_xdr() + |> HashIDPreimageXDR.encode_xdr!() + |> hash() + |> KeyPair.sign(secret_key) + + public_key_map_entry = + SCMapEntry.new( + SCVal.new(symbol: "public_key"), + SCVal.new(bytes: raw_public_key) + ) + + signature_map_entry = + SCMapEntry.new( + SCVal.new(symbol: "signature"), + SCVal.new(bytes: signature) + ) + + signature_sc_val = SCVal.new(map: [public_key_map_entry, signature_map_entry]) + + soroban_address_credentials = %{ + soroban_address_credentials + | signature_args: SCVec.append_sc_val(signature_args, signature_sc_val) + } + + %{credentials | credentials: soroban_address_credentials} + end + + def sign(_args, _secret_key), do: {:error, :invalid_sign_args} + + @spec sign_xdr( + base_64 :: base_64(), + secret_key :: secret_key(), + latest_ledger :: latest_ledger() + ) :: sign_authorization() | error() + def sign_xdr(base_64, secret_key, latest_ledger) + when is_binary(base_64) and is_binary(secret_key) and is_integer(latest_ledger) do + {%SorobanAuthorizationEntry{ + credentials: + %{ + value: + %SorobanAddressCredentialsXDR{ + nonce: nonce + } = soroban_address_credentials + } = credentials, + root_invocation: root_invocation + } = soroban_auth, + ""} = + base_64 + |> Base.decode64!() + |> SorobanAuthorizationEntry.decode_xdr!() + + signature_expiration_ledger = UInt32.new(latest_ledger + 3) + + signature_args = + nonce + |> build_signature_args_from_xdr( + signature_expiration_ledger, + root_invocation, + secret_key + ) + |> SCVal.to_xdr() + |> (&SCVecXDR.new([&1])).() + + soroban_address_credentials = %{ + soroban_address_credentials + | signature_args: signature_args, + signature_expiration_ledger: signature_expiration_ledger + } + + credentials = %{credentials | value: soroban_address_credentials} + + %{soroban_auth | credentials: credentials} + |> SorobanAuthorizationEntry.encode_xdr!() + |> Base.encode64() + end + + def sign_xdr(_base_64, _secret_key, _latest_ledger), do: {:error, :invalid_sign_args} + + @spec network_id_xdr :: binary() + defp network_id_xdr, do: hash(Network.passphrase()) + + @spec hash(data :: binary()) :: binary() + defp hash(data), do: :crypto.hash(:sha256, data) + @spec validate_credentials(credentials :: credentials()) :: validation() defp validate_credentials(%SorobanCredentials{} = credentials), do: {:ok, credentials} defp validate_credentials(_credentials), do: {:error, :invalid_credentials} @@ -65,4 +197,48 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntry do do: {:ok, root_invocation} defp validate_root_invocation(_root_invocation), do: {:error, :invalid_root_invocation} + + @spec build_signature_args_from_xdr( + nonce :: Int64.t(), + signature_expiration_ledger :: UInt32.t(), + root_invocation :: SorobanAuthorizedInvocationXDR.t(), + secret_key :: secret_key() + ) :: SCVal.t() | error() + defp build_signature_args_from_xdr( + nonce, + signature_expiration_ledger, + root_invocation, + secret_key + ) do + {public_key, _secret_key} = KeyPair.from_secret_seed(secret_key) + raw_public_key = KeyPair.raw_public_key(public_key) + envelope_type = EnvelopeType.new(:ENVELOPE_TYPE_SOROBAN_AUTHORIZATION) + + signature = + network_id_xdr() + |> Hash.new() + |> HashIDPreimageSorobanAuthorizationXDR.new( + nonce, + signature_expiration_ledger, + root_invocation + ) + |> HashIDPreimageXDR.new(envelope_type) + |> HashIDPreimageXDR.encode_xdr!() + |> hash() + |> KeyPair.sign(secret_key) + + public_key_map_entry = + SCMapEntry.new( + SCVal.new(symbol: "public_key"), + SCVal.new(bytes: raw_public_key) + ) + + signature_map_entry = + SCMapEntry.new( + SCVal.new(symbol: "signature"), + SCVal.new(bytes: signature) + ) + + SCVal.new(map: [public_key_map_entry, signature_map_entry]) + end end diff --git a/lib/tx_build/variable_opaque.ex b/lib/tx_build/variable_opaque.ex deleted file mode 100644 index b26322ba..00000000 --- a/lib/tx_build/variable_opaque.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Stellar.TxBuild.VariableOpaque do - @moduledoc """ - `VariableOpaque` struct definition. - """ - alias StellarBase.XDR.VariableOpaque - - @behaviour Stellar.TxBuild.XDR - - @type value :: String.t() - - @type t :: %__MODULE__{value: value()} - - defstruct [:value] - - @impl true - def new(value, opts \\ []) - - def new(value, _opts) when is_binary(value), do: %__MODULE__{value: value} - - def new(_value, _opts), do: {:error, :invalid_opaque} - - @impl true - def to_xdr(%__MODULE__{value: value}), do: VariableOpaque.new(value) - def to_xdr(_struct), do: {:error, :invalid_struct} -end diff --git a/test/support/xdr_fixtures.ex b/test/support/xdr_fixtures.ex index b43a3521..0dd2e9e9 100644 --- a/test/support/xdr_fixtures.ex +++ b/test/support/xdr_fixtures.ex @@ -18,6 +18,7 @@ defmodule Stellar.Test.XDRFixtures do alias Stellar.TxBuild.Asset, as: TxAsset alias Stellar.TxBuild.AccountID, as: TxAccountID alias Stellar.TxBuild.SequenceNumber, as: TxSequenceNumber + alias Stellar.TxBuild.SorobanAuthorizationEntry, as: TxSorobanAuthorizationEntry alias StellarBase.XDR.{ AccountID, @@ -809,6 +810,24 @@ defmodule Stellar.Test.XDRFixtures do |> OperationBody.new(op_type) end + @spec invoke_host_function_op_xdr( + function :: TxHostFunction.t(), + auths :: list(TxSorobanAuthorizationEntry.t()) + ) :: OperationBody.t() + def invoke_host_function_op_xdr(function, auths) do + op_type = OperationType.new(:INVOKE_HOST_FUNCTION) + + auths = + auths + |> Enum.map(&TxSorobanAuthorizationEntry.to_xdr/1) + |> SorobanAuthorizationEntryList.new() + + function + |> TxHostFunction.to_xdr() + |> InvokeHostFunction.new(auths) + |> OperationBody.new(op_type) + end + @spec build_asset_xdr(asset :: any()) :: list(Asset.t()) defp build_asset_xdr(:native), do: create_asset_native_xdr() diff --git a/test/tx_build/invoke_host_function_test.exs b/test/tx_build/invoke_host_function_test.exs index 5c7fe452..aba0c8bf 100644 --- a/test/tx_build/invoke_host_function_test.exs +++ b/test/tx_build/invoke_host_function_test.exs @@ -14,7 +14,8 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do SorobanAuthorizationEntry } - import Stellar.Test.XDRFixtures, only: [invoke_host_function_op_xdr: 1] + import Stellar.Test.XDRFixtures, + only: [invoke_host_function_op_xdr: 1, invoke_host_function_op_xdr: 2] describe "InvokeHostFunction" do setup do @@ -57,7 +58,8 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do invoke_host_function_op: invoke_host_function_op, auths: auths, base_64_auths: base_64_auths, - xdr: invoke_host_function_op_xdr(host_function) + xdr: invoke_host_function_op_xdr(host_function), + xdr_with_auth: invoke_host_function_op_xdr(host_function, auths) } end @@ -114,11 +116,26 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do |> InvokeHostFunction.set_auth(:invalid) end + test "new/2 with invalid attributes" do + {:error, :invalid_operation_attributes} = InvokeHostFunction.new(:invalid) + end + test "to_xdr/1", %{ invoke_host_function_op: invoke_host_function_op, xdr: xdr } do ^xdr = InvokeHostFunction.to_xdr(invoke_host_function_op) end + + test "to_xdr/2 with auth entry list", %{ + host_function: host_function, + auths: auths, + xdr_with_auth: xdr_with_auth + } do + ^xdr_with_auth = + [host_function: host_function, auths: auths] + |> InvokeHostFunction.new() + |> InvokeHostFunction.to_xdr() + end end end diff --git a/test/tx_build/soroban_authorization_entry_test.exs b/test/tx_build/soroban_authorization_entry_test.exs index d15c7736..adb0655b 100644 --- a/test/tx_build/soroban_authorization_entry_test.exs +++ b/test/tx_build/soroban_authorization_entry_test.exs @@ -1,14 +1,18 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntryTest do use ExUnit.Case - alias Stellar.TxBuild.SorobanAuthorizedInvocation - alias Stellar.TxBuild.SorobanAuthorizedFunction - alias Stellar.TxBuild.SorobanCredentials - alias Stellar.TxBuild.SorobanAuthorizedContractFunction - alias Stellar.TxBuild.SCVal - alias Stellar.TxBuild.SCVec - alias Stellar.TxBuild.SCAddress - alias Stellar.TxBuild.SorobanAuthorizationEntry + alias Stellar.TxBuild.{ + SCAddress, + SCMapEntry, + SCVal, + SCVec, + SorobanAddressCredentials, + SorobanAuthorizedInvocation, + SorobanAuthorizedFunction, + SorobanAuthorizedContractFunction, + SorobanAuthorizationEntry, + SorobanCredentials + } setup do fn_args = SCVec.new([SCVal.new(symbol: "dev")]) @@ -70,11 +74,38 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntryTest do } } + address = SCAddress.new("GDEU46HFMHBHCSFA3K336I3MJSBZCWVI3LUGSNL6AF2BW2Q2XR7NNAPM") + nonce = 1_078_069_780 + signature_expiration_ledger = 164_080 + + soroban_address_credentials = + SorobanAddressCredentials.new( + address: address, + nonce: nonce, + signature_expiration_ledger: signature_expiration_ledger, + signature_args: SCVec.new([]) + ) + + address_credentials = SorobanCredentials.new(address: soroban_address_credentials) + + soroban_auth_entry_with_address_credentials = + SorobanAuthorizationEntry.new( + credentials: address_credentials, + root_invocation: root_invocation + ) + %{ credentials: credentials, root_invocation: root_invocation, soroban_auth_entry: soroban_auth_entry, - xdr: xdr + xdr: xdr, + base_64: + "AAAAAQAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftY69WqNb/7SRQAAAAAAAAAAAAAAAAAAAAEYaWENrVfrP3DtntX8suGy/f52r9ikRgXsGYWrbdStxwAAAANpbmMAAAAAAgAAABIAAAAAAAAAAMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WAAAACQAAAAAAAAAAAAAAAAAAAAIAAAAA", + secret_key: "SCAVFA3PI3MJLTQNMXOUNBSEUOSY66YMG3T2KCQKLQBENNVLVKNPV3EK", + latest_ledger: 164_256, + sign_xdr: + "AAAAAQAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftY69WqNb/7SRQACgaMAAAABAAAAEQAAAAEAAAACAAAADwAAAApwdWJsaWNfa2V5AAAAAAANAAAAIMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WAAAADwAAAAlzaWduYXR1cmUAAAAAAAANAAAAQOdgROP+0omN51SCii/Ttcy5PhyPfGIaPWG4FBvQNEp1jGMio+lH5IKE5boB5dvdbR0wNixWSHXZBb/35hyftAIAAAAAAAAAARhpYQ2tV+s/cO2e1fyy4bL9/nav2KRGBewZhatt1K3HAAAAA2luYwAAAAACAAAAEgAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftYAAAAJAAAAAAAAAAAAAAAAAAAAAgAAAAA=", + soroban_auth_entry_with_address_credentials: soroban_auth_entry_with_address_credentials } end @@ -99,6 +130,69 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntryTest do {:error, :invalid_auth_entry_args} = SorobanAuthorizationEntry.new(:invalid) end + test "sign/2", %{ + soroban_auth_entry_with_address_credentials: soroban_auth_entry_with_address_credentials, + root_invocation: root_invocation, + secret_key: secret_key + } do + %SorobanAuthorizationEntry{ + credentials: %SorobanAddressCredentials{ + signature_args: %SCVec{ + items: [ + %SCVal{ + type: :map, + value: [ + %SCMapEntry{ + key: %SCVal{type: :symbol, value: "public_key"}, + val: %SCVal{ + type: :bytes, + value: + <<201, 78, 120, 229, 97, 194, 113, 72, 160, 218, 183, 191, 35, 108, 76, 131, + 145, 90, 168, 218, 232, 105, 53, 126, 1, 116, 27, 106, 26, 188, 126, 214>> + } + }, + %SCMapEntry{ + key: %SCVal{type: :symbol, value: "signature"}, + val: %SCVal{ + type: :bytes, + value: + <<150, 185, 157, 21, 98, 125, 110, 204, 42, 246, 50, 2, 183, 10, 131, 52, + 104, 227, 126, 242, 21, 38, 240, 255, 85, 41, 141, 68, 84, 109, 83, 40, + 85, 45, 189, 166, 230, 247, 130, 33, 7, 98, 206, 245, 60, 171, 182, 42, + 10, 185, 218, 200, 114, 119, 66, 120, 20, 170, 133, 131, 105, 148, 91, + 14>> + } + } + ] + } + ] + } + }, + root_invocation: ^root_invocation + } = SorobanAuthorizationEntry.sign(soroban_auth_entry_with_address_credentials, secret_key) + end + + test "sign/2 invalid secret_key", %{ + soroban_auth_entry_with_address_credentials: soroban_auth_entry_with_address_credentials + } do + {:error, :invalid_sign_args} = + SorobanAuthorizationEntry.sign(soroban_auth_entry_with_address_credentials, :secret_key) + end + + test "sign_xdr/3", %{ + base_64: base_64, + secret_key: secret_key, + latest_ledger: latest_ledger, + sign_xdr: sign_xdr + } do + ^sign_xdr = SorobanAuthorizationEntry.sign_xdr(base_64, secret_key, latest_ledger) + end + + test "sign_xdr/3 invalid secret_key", %{base_64: base_64, latest_ledger: latest_ledger} do + {:error, :invalid_sign_args} = + SorobanAuthorizationEntry.sign_xdr(base_64, :secret_key, latest_ledger) + end + test "to_xdr/1", %{soroban_auth_entry: soroban_auth_entry, xdr: xdr} do ^xdr = SorobanAuthorizationEntry.to_xdr(soroban_auth_entry) end From 08a90fa9baefd178f848401b44328cd942573f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rold=C3=A1n?= <62166813+Odraxs@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:11:05 -0500 Subject: [PATCH 3/6] BumpFootprintExpiration operation (#323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Soroban transaction data needed structs * Add tests and minor changes Co-authored-by: David Roldán * Add documentation and functionality changes * Add requested changes and update stellar_base dep * Update example info and add missing comma in alias --------- Co-authored-by: Edwin Steven Guayacan --- docs/README.md | 1 + .../soroban/bump_footprint_expiration.md | 96 +++++++++++ lib/tx_build/bump_footprint_expiration.ex | 55 +++++++ lib/tx_build/ledger/contract_code.ex | 60 +++++++ lib/tx_build/ledger/contract_data.ex | 90 ++++++++++ lib/tx_build/ledger_footprint.ex | 71 ++++++++ lib/tx_build/ledger_key.ex | 54 +++++- lib/tx_build/operation.ex | 3 + lib/tx_build/soroban_resources.ex | 90 ++++++++++ lib/tx_build/soroban_transaction_data.ex | 64 ++++++++ mix.lock | 6 +- test/support/fixtures/xdr.ex | 2 + test/support/fixtures/xdr/ledger.ex | 34 ++++ .../bump_footprint_expiration_test.exs | 42 +++++ test/tx_build/ledger/contract_code_test.exs | 55 +++++++ test/tx_build/ledger/contract_data_test.exs | 154 ++++++++++++++++++ test/tx_build/ledger_footprint_test.exs | 89 ++++++++++ test/tx_build/ledger_key_test.exs | 91 ++++++++++- test/tx_build/soroba_tarsaction_data_test.exs | 113 +++++++++++++ test/tx_build/soroban_resources_test.exs | 147 +++++++++++++++++ 20 files changed, 1310 insertions(+), 7 deletions(-) create mode 100644 docs/examples/soroban/bump_footprint_expiration.md create mode 100644 lib/tx_build/bump_footprint_expiration.ex create mode 100644 lib/tx_build/ledger/contract_code.ex create mode 100644 lib/tx_build/ledger/contract_data.ex create mode 100644 lib/tx_build/ledger_footprint.ex create mode 100644 lib/tx_build/soroban_resources.ex create mode 100644 lib/tx_build/soroban_transaction_data.ex create mode 100644 test/tx_build/bump_footprint_expiration_test.exs create mode 100644 test/tx_build/ledger/contract_code_test.exs create mode 100644 test/tx_build/ledger/contract_data_test.exs create mode 100644 test/tx_build/ledger_footprint_test.exs create mode 100644 test/tx_build/soroba_tarsaction_data_test.exs create mode 100644 test/tx_build/soroban_resources_test.exs diff --git a/docs/README.md b/docs/README.md index 53e2c2a8..b3ba17e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,3 +16,4 @@ * [Upload Contract Code (WASM)](/docs/examples/soroban/upload_contract_code.md) * [Create Contract](/docs/examples/soroban/create_contract.md) * [Invoke Contract Function](/docs/examples/soroban/invoke_contract_functions.md) +* [Bump Footprint Expiration](/docs/examples/soroban/bump_footprint_expiration.md) diff --git a/docs/examples/soroban/bump_footprint_expiration.md b/docs/examples/soroban/bump_footprint_expiration.md new file mode 100644 index 00000000..3875c7c7 --- /dev/null +++ b/docs/examples/soroban/bump_footprint_expiration.md @@ -0,0 +1,96 @@ +# Bump Footprint Expiration +The `BumpFootprintExpirationOp` operation is used to extend a contract data entry's lifetime. + +A contract instance, wasm hash, and data storage entry (persistent/instance/temporary) can expire, so you can use this bump operation to extend its lifetime. +Read more about it: +- https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#bumpfootprintexpirationop +- https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html + +In this example, we will bump the contract instance of an already deployed contract in the network, adding 1000 ledgers to it. + +> **Warning** +> Please note that Soroban is still under development, so breaking changes may occur. + +> **Note** +> All this actions require to use `simulateTransaction` and `sendTransaction` RPC endpoints when specified in the code comments to achieve the bump footprint expiration. + +```elixir +alias Stellar.TxBuild.{ + Account, + BaseFee, + BumpFootprintExpiration, + LedgerKey, + SCAddress, + SCVal, + SequenceNumber, + Signature, + SorobanResources, + SorobanTransactionData +} + +alias Stellar.KeyPair + +contract_address = "CAMGSYINVVL6WP3Q5WPNL7FS4GZP37TWV7MKIRQF5QMYLK3N2SW4P3RC" +contract_sc_address = SCAddress.new(contract_address) +key = SCVal.new(ledger_key_contract_instance: nil) + +keypair = + {public_key, _secret} = + Stellar.KeyPair.from_secret_seed("SCAVFA3PI3MJLTQNMXOUNBSEUOSY66YMG3T2KCQKLQBENNVLVKNPV3EK") + +contract_data = + LedgerKey.new( + {:contract_data, + [ + contract: contract_sc_address, + key: key, + durability: :persistent, + body_type: :data_entry + ]} + ) + +hash= StellarBase.StrKey.decode!(contract_address, :contract) +contract_code = LedgerKey.new({:contract_code, [hash: hash, body_type: :data_entry]}) +footprint = LedgerFootprint.new(read_only: [contract_data, contract_code]) + +soroban_data = +[ + footprint: footprint, + instructions: 0, + read_bytes: 0, + write_bytes: 0, + extended_meta_data_size_bytes: 0 +] +|> SorobanResources.new() +|> (&SorobanTransactionData.new(resources: &1, refundable_fee: 0)).() +|> SorobanTransactionData.to_xdr() + +source_account = Account.new(public_key) +{:ok, seq_num} = Accounts.fetch_next_sequence_number(public_key) +sequence_number = SequenceNumber.new(seq_num) +signature = Signature.new(keypair) +bump_footprint_op = BumpFootprintExpiration.new(ledgers_to_expire: 1000) + +# Use this XDR to simulate the transaction and get the soroban_data and min_resource_fee +source_account +|> Stellar.TxBuild.new(sequence_number: sequence_number) +|> Stellar.TxBuild.add_operation(bump_footprint_op) +|> Stellar.TxBuild.envelope() + +# Simulate Transaction +soroban_data = + "AAAAAAAAAAIAAAAGAAAAARhpYQ2tV+s/cO2e1fyy4bL9/nav2KRGBewZhatt1K3HAAAAFAAAAAEAAAAAAAAABxhpYQ2tV+s/cO2e1fyy4bL9/nav2KRGBewZhatt1K3HAAAAAAAAAAAAAAAAAAABLAAAAAAAAAJYAAAAAAAAAHY=" + +min_resource_fee = 11_516 +fee = BaseFee.new(min_resource_fee + 100) + +# Use the XDR generated here to send it to the futurenet +source_account +|> Stellar.TxBuild.new(sequence_number: sequence_number) +|> Stellar.TxBuild.add_operation(bump_footprint_op) +|> Stellar.TxBuild.set_soroban_data(soroban_data) +|> Stellar.TxBuild.set_base_fee(fee) +|> Stellar.TxBuild.sign(signature) +|> Stellar.TxBuild.envelope() + +``` diff --git a/lib/tx_build/bump_footprint_expiration.ex b/lib/tx_build/bump_footprint_expiration.ex new file mode 100644 index 00000000..3b7f7f1f --- /dev/null +++ b/lib/tx_build/bump_footprint_expiration.ex @@ -0,0 +1,55 @@ +defmodule Stellar.TxBuild.BumpFootprintExpiration do + @moduledoc """ + `BumpFootprintExpiration` struct definition. + """ + import Stellar.TxBuild.Validations, only: [validate_optional_account: 1] + + alias Stellar.TxBuild.OptionalAccount + + alias StellarBase.XDR.{ + ExtensionPoint, + OperationBody, + OperationType, + Operations.BumpFootprintExpiration, + UInt32, + Void + } + + @behaviour Stellar.TxBuild.XDR + + @type ledgers_to_expire :: integer() + + @type t :: %__MODULE__{ + ledgers_to_expire: ledgers_to_expire(), + source_account: OptionalAccount.t() + } + + defstruct [:ledgers_to_expire, :source_account] + + @impl true + def new(args, opts \\ []) + + def new(args, _opts) when is_list(args) do + ledgers_to_expire = Keyword.get(args, :ledgers_to_expire, 1) + source_account = Keyword.get(args, :source_account) + + with {:ok, source_account} <- validate_optional_account({:source_account, source_account}) do + %__MODULE__{ledgers_to_expire: ledgers_to_expire, source_account: source_account} + end + end + + def new(_value, _opts), do: {:error, :invalid_bump_footprint_op} + + @impl true + def to_xdr(%__MODULE__{ledgers_to_expire: ledgers_to_expire}) do + op_type = OperationType.new(:BUMP_FOOTPRINT_EXPIRATION) + ledgers_to_expire = UInt32.new(ledgers_to_expire) + + Void.new() + |> ExtensionPoint.new(0) + |> BumpFootprintExpiration.new(ledgers_to_expire) + |> OperationBody.new(op_type) + end + + def to_xdr(_struct), do: {:error, :invalid_struct} +end diff --git a/lib/tx_build/ledger/contract_code.ex b/lib/tx_build/ledger/contract_code.ex new file mode 100644 index 00000000..16dd8dde --- /dev/null +++ b/lib/tx_build/ledger/contract_code.ex @@ -0,0 +1,60 @@ +defmodule Stellar.TxBuild.Ledger.ContractCode do + @moduledoc """ + `ContractCode` struct definition. + """ + + alias StellarBase.XDR.{ContractEntryBodyType, Hash, LedgerKeyContractCode} + + @behaviour Stellar.TxBuild.XDR + + @type error :: {:error, atom()} + @type validation :: {:ok, any()} | error() + @type hash :: String.t() + @type body_type :: :data_entry | :expiration_ext + + @type t :: %__MODULE__{ + hash: hash(), + body_type: body_type() + } + + defstruct [:hash, :body_type] + + @allowed_body_types ~w(data_entry expiration_ext)a + + @impl true + def new(args, opts \\ []) + + def new( + [ + {:hash, hash}, + {:body_type, body_type} + ], + _opts + ) + when is_binary(hash) and body_type in @allowed_body_types do + %__MODULE__{ + hash: hash, + body_type: body_type + } + end + + def new(_value, _opts), do: {:error, :invalid_ledger_key_args} + + @impl true + def to_xdr(%__MODULE__{ + hash: hash, + body_type: body_type + }) do + body_type = body_type_to_xdr(body_type) + + hash + |> Hash.new() + |> LedgerKeyContractCode.new(body_type) + end + + def to_xdr(_struct), do: {:error, :invalid_struct} + + @spec body_type_to_xdr(atom()) :: ContractEntryBodyType.t() + defp body_type_to_xdr(:data_entry), do: ContractEntryBodyType.new(:DATA_ENTRY) + defp body_type_to_xdr(:expiration_ext), do: ContractEntryBodyType.new(:EXPIRATION_EXTENSION) +end diff --git a/lib/tx_build/ledger/contract_data.ex b/lib/tx_build/ledger/contract_data.ex new file mode 100644 index 00000000..7187c1ee --- /dev/null +++ b/lib/tx_build/ledger/contract_data.ex @@ -0,0 +1,90 @@ +defmodule Stellar.TxBuild.Ledger.ContractData do + @moduledoc """ + `ContractData` struct definition. + """ + + import Stellar.TxBuild.Validations, + only: [validate_address: 1] + + alias Stellar.TxBuild.{SCAddress, SCVal} + alias StellarBase.XDR.{ContractEntryBodyType, ContractDataDurability, LedgerKeyContractData} + + @behaviour Stellar.TxBuild.XDR + + @type error :: {:error, atom()} + @type validation :: {:ok, any()} | error() + @type contract :: SCAddress.t() + @type key :: SCVal.t() + @type durability :: :temporary | :persistent + @type body_type :: :data_entry | :expiration_ext + + @type t :: %__MODULE__{ + contract: contract(), + key: key(), + durability: durability(), + body_type: body_type() + } + + defstruct [:contract, :key, :durability, :body_type] + + @allowed_durabilities ~w(temporary persistent)a + @allowed_body_types ~w(data_entry expiration_ext)a + + @impl true + def new(args, opts \\ []) + + def new( + [ + {:contract, contract}, + {:key, key}, + {:durability, durability}, + {:body_type, body_type} + ], + _opts + ) + when durability in @allowed_durabilities and body_type in @allowed_body_types do + with {:ok, contract} <- validate_address(contract), + {:ok, key} <- validate_sc_val_ledger_instance(key) do + %__MODULE__{ + contract: contract, + key: key, + durability: durability, + body_type: body_type + } + end + end + + def new(_value, _opts), do: {:error, :invalid_ledger_key_args} + + @impl true + def to_xdr(%__MODULE__{ + contract: contract, + key: key, + durability: durability, + body_type: body_type + }) do + key = SCVal.to_xdr(key) + durability = durability_to_xdr(durability) + body_type = body_type_to_xdr(body_type) + + contract + |> SCAddress.to_xdr() + |> LedgerKeyContractData.new(key, durability, body_type) + end + + def to_xdr(_struct), do: {:error, :invalid_struct} + + @spec validate_sc_val_ledger_instance(key :: key()) :: validation() + defp validate_sc_val_ledger_instance(%SCVal{type: :ledger_key_contract_instance} = key), + do: {:ok, key} + + defp validate_sc_val_ledger_instance(_key), do: {:error, :invalid_key} + + @spec durability_to_xdr(atom()) :: ContractDataDurability.t() + defp durability_to_xdr(:temporary), do: ContractDataDurability.new(:TEMPORARY) + defp durability_to_xdr(:persistent), do: ContractDataDurability.new(:PERSISTENT) + + @spec body_type_to_xdr(atom()) :: ContractEntryBodyType.t() + defp body_type_to_xdr(:data_entry), do: ContractEntryBodyType.new(:DATA_ENTRY) + defp body_type_to_xdr(:expiration_ext), do: ContractEntryBodyType.new(:EXPIRATION_EXTENSION) +end diff --git a/lib/tx_build/ledger_footprint.ex b/lib/tx_build/ledger_footprint.ex new file mode 100644 index 00000000..cef0b170 --- /dev/null +++ b/lib/tx_build/ledger_footprint.ex @@ -0,0 +1,71 @@ +defmodule Stellar.TxBuild.LedgerFootprint do + @moduledoc """ + `LedgerFootprint` struct definition. + """ + + alias Stellar.TxBuild.LedgerKey + alias StellarBase.XDR.{LedgerFootprint, LedgerKeyList} + + @behaviour Stellar.TxBuild.XDR + + @type error :: {:error, atom()} + @type validation :: {:ok, any()} | error() + @type ledger_key :: LedgerKey.t() + @type ledger_keys :: list(ledger_key) + + @type t :: %__MODULE__{ + read_only: ledger_keys(), + read_write: ledger_keys() + } + + defstruct [:read_only, :read_write] + + @impl true + def new(args \\ [], opts \\ []) + + def new(args, _opts) when is_list(args) do + read_only = Keyword.get(args, :read_only, []) + read_write = Keyword.get(args, :read_write, []) + + with {:ok, read_only} <- validate_ledger_keys(read_only), + {:ok, read_write} <- validate_ledger_keys(read_write) do + %__MODULE__{ + read_only: read_only, + read_write: read_write + } + end + end + + def new(_value, _opts), do: {:error, :invalid_ledger_footprint} + + @impl true + def to_xdr(%__MODULE__{read_only: read_only, read_write: read_write}) do + read_write = to_ledger_key_list(read_write) + + read_only + |> to_ledger_key_list() + |> LedgerFootprint.new(read_write) + end + + def to_xdr(_struct), do: {:error, :invalid_struct} + + @spec validate_ledger_keys(keys :: ledger_keys()) :: validation() + defp validate_ledger_keys(keys) when is_list(keys) do + if Enum.all?(keys, &validate_ledger_key/1), + do: {:ok, keys}, + else: {:error, :invalid_ledger_keys} + end + + defp validate_ledger_keys(_keys), do: {:error, :invalid_ledger_keys} + + @spec validate_ledger_key(ledger_key()) :: boolean() + defp validate_ledger_key(%LedgerKey{}), do: true + defp validate_ledger_key(_key), do: false + + @spec to_ledger_key_list(keys :: ledger_keys()) :: LedgerKeyList.t() + defp to_ledger_key_list(keys) do + keys + |> Enum.map(&LedgerKey.to_xdr/1) + |> LedgerKeyList.new() + end +end diff --git a/lib/tx_build/ledger_key.ex b/lib/tx_build/ledger_key.ex index 5fc61b64..780ebf2e 100644 --- a/lib/tx_build/ledger_key.ex +++ b/lib/tx_build/ledger_key.ex @@ -2,12 +2,32 @@ defmodule Stellar.TxBuild.LedgerKey do @moduledoc """ `LedgerKey` struct definition. """ + alias StellarBase.XDR.{LedgerEntryType, LedgerKey} - alias Stellar.TxBuild.Ledger.{Account, ClaimableBalance, Data, LiquidityPool, Offer, Trustline} + + alias Stellar.TxBuild.Ledger.{ + Account, + ClaimableBalance, + Data, + ContractCode, + ContractData, + LiquidityPool, + Offer, + Trustline + } @behaviour Stellar.TxBuild.XDR - @type type :: :account | :trustline | :offer | :data | :claimable_balance | :liquidity_pool + @type type :: + :account + | :trustline + | :offer + | :data + | :claimable_balance + | :liquidity_pool + | :contract_data + | :contract_code + @type entry :: Account.t() | ClaimableBalance.t() @@ -15,6 +35,8 @@ defmodule Stellar.TxBuild.LedgerKey do | LiquidityPool.t() | Offer.t() | Trustline.t() + | ContractData.t() + | ContractCode.t() @type t :: %__MODULE__{type: type(), entry: entry()} @@ -74,6 +96,32 @@ defmodule Stellar.TxBuild.LedgerKey do end end + def new( + {:contract_data, args}, + _opts + ) do + case ContractData.new(args) do + %ContractData{} = contract_data -> + %__MODULE__{type: :contract_data, entry: contract_data} + + _error -> + {:error, :invalid_contract_data} + end + end + + def new( + {:contract_code, args}, + _opts + ) do + case ContractCode.new(args) do + %ContractCode{} = contract_code -> + %__MODULE__{type: :contract_code, entry: contract_code} + + _error -> + {:error, :invalid_contract_code} + end + end + def new(_args, _opts), do: {:error, :invalid_ledger_key} @impl true @@ -92,4 +140,6 @@ defmodule Stellar.TxBuild.LedgerKey do defp ledger_entry_type(:data), do: LedgerEntryType.new(:DATA) defp ledger_entry_type(:claimable_balance), do: LedgerEntryType.new(:CLAIMABLE_BALANCE) defp ledger_entry_type(:liquidity_pool), do: LedgerEntryType.new(:LIQUIDITY_POOL) + defp ledger_entry_type(:contract_data), do: LedgerEntryType.new(:CONTRACT_DATA) + defp ledger_entry_type(:contract_code), do: LedgerEntryType.new(:CONTRACT_CODE) end diff --git a/lib/tx_build/operation.ex b/lib/tx_build/operation.ex index b70d063a..39537efb 100644 --- a/lib/tx_build/operation.ex +++ b/lib/tx_build/operation.ex @@ -5,6 +5,7 @@ defmodule Stellar.TxBuild.Operation do alias Stellar.TxBuild.{ AccountMerge, + BumpFootprintExpiration, BumpSequence, BeginSponsoringFutureReserves, ChangeTrust, @@ -58,6 +59,7 @@ defmodule Stellar.TxBuild.Operation do | SetOptions.t() | SetTrustlineFlags.t() | InvokeHostFunction.t() + | BumpFootprintExpiration.t() @type t :: %__MODULE__{body: operation(), source_account: OptionalAccount.t()} @@ -86,6 +88,7 @@ defmodule Stellar.TxBuild.Operation do defp validate_operation(%{__struct__: op_type} = op_body) do op_types = [ AccountMerge, + BumpFootprintExpiration, BumpSequence, BeginSponsoringFutureReserves, ChangeTrust, diff --git a/lib/tx_build/soroban_resources.ex b/lib/tx_build/soroban_resources.ex new file mode 100644 index 00000000..57952487 --- /dev/null +++ b/lib/tx_build/soroban_resources.ex @@ -0,0 +1,90 @@ +defmodule Stellar.TxBuild.SorobanResources do + @moduledoc """ + `SorobanResources` struct definition. + """ + alias StellarBase.XDR.{SorobanResources, UInt32} + alias Stellar.TxBuild.LedgerFootprint + + @behaviour Stellar.TxBuild.XDR + + @type error :: {:error, atom()} + @type validation :: {:ok, any()} | error() + @type footprint :: LedgerFootprint.t() + @type instructions :: integer() + @type read_bytes :: integer() + @type write_bytes :: integer() + @type extended_meta_data_size_bytes :: integer() + + @type t :: %__MODULE__{ + footprint: footprint(), + instructions: instructions(), + read_bytes: read_bytes(), + write_bytes: write_bytes(), + extended_meta_data_size_bytes: extended_meta_data_size_bytes() + } + + defstruct [ + :footprint, + :instructions, + :read_bytes, + :write_bytes, + :extended_meta_data_size_bytes + ] + + @impl true + def new(args, opts \\ []) + + def new( + [ + {:footprint, footprint}, + {:instructions, instructions}, + {:read_bytes, read_bytes}, + {:write_bytes, write_bytes}, + {:extended_meta_data_size_bytes, extended_meta_data_size_bytes} + ], + _opts + ) + when is_integer(instructions) and is_integer(read_bytes) and is_integer(write_bytes) and + is_integer(extended_meta_data_size_bytes) do + with {:ok, footprint} <- validate_footprint(footprint) do + %__MODULE__{ + footprint: footprint, + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + } + end + end + + def new(_value, _opts), do: {:error, :invalid_soroban_resources_args} + + @impl true + def to_xdr(%__MODULE__{ + footprint: footprint, + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + }) do + instructions = UInt32.new(instructions) + read_bytes = UInt32.new(read_bytes) + write_bytes = UInt32.new(write_bytes) + extended_meta_data_size_bytes = UInt32.new(extended_meta_data_size_bytes) + + footprint + |> LedgerFootprint.to_xdr() + |> SorobanResources.new( + instructions, + read_bytes, + write_bytes, + extended_meta_data_size_bytes + ) + end + + def to_xdr(_struct), do: {:error, :invalid_struct} + + @spec validate_footprint(footprint :: footprint()) :: validation() + defp validate_footprint(%LedgerFootprint{} = footprint), do: {:ok, footprint} + defp validate_footprint(_footprint), do: {:error, :invalid_footprint} +end diff --git a/lib/tx_build/soroban_transaction_data.ex b/lib/tx_build/soroban_transaction_data.ex new file mode 100644 index 00000000..0f07677a --- /dev/null +++ b/lib/tx_build/soroban_transaction_data.ex @@ -0,0 +1,64 @@ +defmodule Stellar.TxBuild.SorobanTransactionData do + @moduledoc """ + `SorobanTransactionData` struct definition. + """ + alias StellarBase.XDR.{ + ExtensionPoint, + Int64, + SorobanTransactionData, + Void + } + + alias Stellar.TxBuild.SorobanResources + + @behaviour Stellar.TxBuild.XDR + + @type error :: {:error, atom()} + @type validation :: {:ok, any()} | error() + @type resources :: SorobanResources.t() + @type refundable_fee :: non_neg_integer() + + @type t :: %__MODULE__{ + resources: resources(), + refundable_fee: refundable_fee() + } + + defstruct [:resources, :refundable_fee] + + @impl true + def new(args, opts \\ []) + + def new( + [ + {:resources, resources}, + {:refundable_fee, refundable_fee} + ], + _opts + ) + when is_integer(refundable_fee) and refundable_fee >= 0 do + with {:ok, resources} <- validate_resources(resources) do + %__MODULE__{ + resources: resources, + refundable_fee: refundable_fee + } + end + end + + def new(_args, _opts), do: {:error, :invalid_soroban_transaction_data} + + @impl true + def to_xdr(%__MODULE__{resources: resources, refundable_fee: refundable_fee}) do + resources = SorobanResources.to_xdr(resources) + refundable_fee = Int64.new(refundable_fee) + + Void.new() + |> ExtensionPoint.new(0) + |> SorobanTransactionData.new(resources, refundable_fee) + end + + def to_xdr(_struct), do: {:error, :invalid_struct} + + @spec validate_resources(resources :: resources()) :: validation() + defp validate_resources(%SorobanResources{} = resources), do: {:ok, resources} + defp validate_resources(_resources), do: {:error, :invalid_resources} +end diff --git a/mix.lock b/mix.lock index 542c907a..49642817 100644 --- a/mix.lock +++ b/mix.lock @@ -14,7 +14,7 @@ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, @@ -22,7 +22,7 @@ "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "stellar_base": {:hex, :stellar_base, "0.13.0", "75bebe0401225df40336e7c510f5c36dda6ad7486011d43a63d55c5a9bc5fb14", [:mix], [{:crc, "~> 0.10.0", [hex: :crc, repo: "hexpm", optional: false]}, {:elixir_xdr, "~> 0.3.9", [hex: :elixir_xdr, repo: "hexpm", optional: false]}], "hexpm", "e92b5ca2a01bf2a4cf75bb699139626d46a3680932b2b93b594c9d54781d5827"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "stellar_base": {:hex, :stellar_base, "0.13.1", "ac3d1d6b5ad131eb27fbbdd8bfb42f998ea14c48e341a5cb091aa05a80c62deb", [:mix], [{:crc, "~> 0.10.0", [hex: :crc, repo: "hexpm", optional: false]}, {:elixir_xdr, "~> 0.3.9", [hex: :elixir_xdr, repo: "hexpm", optional: false]}], "hexpm", "8b72a9e0bb1cb13a031ace0c49ae51787ddd616f9d6333c2a3411cab068bc9c1"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/support/fixtures/xdr.ex b/test/support/fixtures/xdr.ex index fd44a9fd..041a70ac 100644 --- a/test/support/fixtures/xdr.ex +++ b/test/support/fixtures/xdr.ex @@ -88,6 +88,8 @@ defmodule Stellar.Test.Fixtures.XDR do defdelegate ledger_key_data(account_id, data_name), to: Ledger defdelegate ledger_key_claimable_balance(claimable_balance_id), to: Ledger defdelegate ledger_key_liquidity_pool(liquidity_pool_id), to: Ledger + defdelegate ledger_key_contract_data(), to: Ledger + defdelegate ledger_key_contract_code(), to: Ledger defdelegate revoke_sponsorship(type, attrs), to: Ledger end diff --git a/test/support/fixtures/xdr/ledger.ex b/test/support/fixtures/xdr/ledger.ex index 13a0a1ea..ca2fcc43 100644 --- a/test/support/fixtures/xdr/ledger.ex +++ b/test/support/fixtures/xdr/ledger.ex @@ -347,6 +347,40 @@ defmodule Stellar.Test.Fixtures.XDR.Ledger do } end + @spec ledger_key_contract_data() :: LedgerKey.t() + def ledger_key_contract_data do + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractData{ + contract: %StellarBase.XDR.SCAddress{ + sc_address: %StellarBase.XDR.Hash{ + value: + <<136, 199, 21, 221, 153, 62, 83, 56, 9, 112, 55, 17, 157, 226, 244, 109, 177, 22, + 241, 170, 12, 226, 193, 229, 39, 50, 103, 40, 26, 129, 1, 237>> + }, + type: %StellarBase.XDR.SCAddressType{identifier: :SC_ADDRESS_TYPE_CONTRACT} + }, + key: %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.Void{value: nil}, + type: %StellarBase.XDR.SCValType{identifier: :SCV_LEDGER_KEY_CONTRACT_INSTANCE} + }, + durability: %StellarBase.XDR.ContractDataDurability{identifier: :PERSISTENT}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_DATA} + } + end + + @spec ledger_key_contract_code() :: LedgerKey.t() + def ledger_key_contract_code do + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + } + end + @spec revoke_sponsorship(type :: atom(), args :: Keyword.t()) :: RevokeSponsorship.t() def revoke_sponsorship(:account, account_id: "GD726E62G6G4ANHWHIQTH5LNMFVF2EQSEXITB6DZCCTKVU6EQRRE2SJS" diff --git a/test/tx_build/bump_footprint_expiration_test.exs b/test/tx_build/bump_footprint_expiration_test.exs new file mode 100644 index 00000000..e23303f0 --- /dev/null +++ b/test/tx_build/bump_footprint_expiration_test.exs @@ -0,0 +1,42 @@ +defmodule Stellar.TxBuild.BumpFootprintExpirationTest do + use ExUnit.Case + + alias Stellar.TxBuild.BumpFootprintExpiration + + setup do + ledgers_to_expire = 100 + + %{ + ledgers_to_expire: ledgers_to_expire + } + end + + test "new/1", %{ledgers_to_expire: ledgers_to_expire} do + %BumpFootprintExpiration{ledgers_to_expire: ^ledgers_to_expire} = + BumpFootprintExpiration.new(ledgers_to_expire: ledgers_to_expire) + end + + test "new/1 with invalid ledgers_to_expire" do + {:error, :invalid_bump_footprint_op} = BumpFootprintExpiration.new("ABC") + end + + test "to_xdr/1", %{ledgers_to_expire: ledgers_to_expire} do + %StellarBase.XDR.OperationBody{ + value: %StellarBase.XDR.Operations.BumpFootprintExpiration{ + ext: %StellarBase.XDR.ExtensionPoint{ + extension_point: %StellarBase.XDR.Void{value: nil}, + type: 0 + }, + ledgers_to_expire: %StellarBase.XDR.UInt32{datum: 100} + }, + type: %StellarBase.XDR.OperationType{identifier: :BUMP_FOOTPRINT_EXPIRATION} + } = + ledgers_to_expire + |> (&BumpFootprintExpiration.new(ledgers_to_expire: &1)).() + |> BumpFootprintExpiration.to_xdr() + end + + test "to_xdr/1 with invalid struct" do + {:error, :invalid_struct} = BumpFootprintExpiration.to_xdr(%{}) + end +end diff --git a/test/tx_build/ledger/contract_code_test.exs b/test/tx_build/ledger/contract_code_test.exs new file mode 100644 index 00000000..38e308be --- /dev/null +++ b/test/tx_build/ledger/contract_code_test.exs @@ -0,0 +1,55 @@ +defmodule Stellar.TxBuild.Ledger.ContractCodeTest do + use ExUnit.Case + + alias Stellar.TxBuild.Ledger.ContractCode + + setup do + hash = "ABC123" + body_type_data_entry = :data_entry + body_type_expiration_ext = :expiration_ext + + %{ + hash: hash, + body_type_data_entry: body_type_data_entry, + body_type_expiration_ext: body_type_expiration_ext + } + end + + test "new/1 data_entry", %{hash: hash, body_type_data_entry: body_type} do + %ContractCode{hash: ^hash, body_type: ^body_type} = + ContractCode.new(hash: hash, body_type: body_type) + end + + test "new/1 expiration_ext", %{hash: hash, body_type_expiration_ext: body_type} do + %ContractCode{hash: ^hash, body_type: ^body_type} = + ContractCode.new([{:hash, hash}, {:body_type, body_type}]) + end + + test "new/1 with invalid args" do + {:error, :invalid_ledger_key_args} = ContractCode.new("ABC", "ABC") + end + + test "to_xdr/1 data_entry", %{hash: hash, body_type_data_entry: body_type} do + %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + } = + [{:hash, hash}, {:body_type, body_type}] + |> ContractCode.new() + |> ContractCode.to_xdr() + end + + test "to_xdr/1 expiration_ext", %{hash: hash, body_type_expiration_ext: body_type} do + %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :EXPIRATION_EXTENSION} + } = + [{:hash, hash}, {:body_type, body_type}] + |> ContractCode.new() + |> ContractCode.to_xdr() + end + + test "to_xdr/1 with invalid struct" do + {:error, :invalid_struct} = ContractCode.to_xdr(%{}) + end +end diff --git a/test/tx_build/ledger/contract_data_test.exs b/test/tx_build/ledger/contract_data_test.exs new file mode 100644 index 00000000..2c9c6ed6 --- /dev/null +++ b/test/tx_build/ledger/contract_data_test.exs @@ -0,0 +1,154 @@ +defmodule Stellar.TxBuild.Ledger.ContractDataTest do + use ExUnit.Case + + alias Stellar.TxBuild.SCAddress + alias Stellar.TxBuild.Ledger.ContractData + + setup do + contract = SCAddress.new("CCEMOFO5TE7FGOAJOA3RDHPC6RW3CFXRVIGOFQPFE4ZGOKA2QEA636SN") + key = %Stellar.TxBuild.SCVal{type: :ledger_key_contract_instance} + durability_temporary = :temporary + durability_persistent = :persistent + body_type_data_entry = :data_entry + body_type_expiration_ext = :expiration_ext + + %{ + contract: contract, + key: key, + durability_temporary: durability_temporary, + durability_persistent: durability_persistent, + body_type_data_entry: body_type_data_entry, + body_type_expiration_ext: body_type_expiration_ext + } + end + + test "new/1 data_entry", %{ + contract: contract, + key: key, + durability_temporary: durability, + body_type_data_entry: body_type + } do + %ContractData{ + contract: ^contract, + key: ^key, + durability: ^durability, + body_type: ^body_type + } = + ContractData.new( + contract: contract, + key: key, + durability: durability, + body_type: body_type + ) + end + + test "new/1 expiration_ext", %{ + contract: contract, + key: key, + durability_persistent: durability, + body_type_expiration_ext: body_type + } do + %ContractData{ + contract: ^contract, + key: ^key, + durability: ^durability, + body_type: ^body_type + } = + ContractData.new( + contract: contract, + key: key, + durability: durability, + body_type: body_type + ) + end + + test "new/1 with invalid args" do + {:error, :invalid_ledger_key_args} = + ContractData.new( + contract: "ABC", + key: "ABC", + durability: "invalid_durability", + body_type: "invalid_body_type" + ) + end + + test "new/1 with invalid key", %{ + contract: contract, + durability_persistent: durability, + body_type_expiration_ext: body_type + } do + {:error, :invalid_key} = + ContractData.new( + contract: contract, + key: "invalid_key", + durability: durability, + body_type: body_type + ) + end + + test "to_xdr/1 data_entry", %{ + contract: contract, + key: key, + durability_temporary: durability, + body_type_data_entry: body_type + } do + %StellarBase.XDR.LedgerKeyContractData{ + contract: %StellarBase.XDR.SCAddress{ + sc_address: %StellarBase.XDR.Hash{ + value: + <<136, 199, 21, 221, 153, 62, 83, 56, 9, 112, 55, 17, 157, 226, 244, 109, 177, 22, + 241, 170, 12, 226, 193, 229, 39, 50, 103, 40, 26, 129, 1, 237>> + }, + type: %StellarBase.XDR.SCAddressType{identifier: :SC_ADDRESS_TYPE_CONTRACT} + }, + key: %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.Void{value: nil}, + type: %StellarBase.XDR.SCValType{identifier: :SCV_LEDGER_KEY_CONTRACT_INSTANCE} + }, + durability: %StellarBase.XDR.ContractDataDurability{identifier: :TEMPORARY}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + } = + ContractData.new( + contract: contract, + key: key, + durability: durability, + body_type: body_type + ) + |> ContractData.to_xdr() + end + + test "to_xdr/1 expiration_ext", %{ + contract: contract, + key: key, + durability_persistent: durability, + body_type_expiration_ext: body_type + } do + %StellarBase.XDR.LedgerKeyContractData{ + contract: %StellarBase.XDR.SCAddress{ + sc_address: %StellarBase.XDR.Hash{ + value: + <<136, 199, 21, 221, 153, 62, 83, 56, 9, 112, 55, 17, 157, 226, 244, 109, 177, 22, + 241, 170, 12, 226, 193, 229, 39, 50, 103, 40, 26, 129, 1, 237>> + }, + type: %StellarBase.XDR.SCAddressType{identifier: :SC_ADDRESS_TYPE_CONTRACT} + }, + key: %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.Void{value: nil}, + type: %StellarBase.XDR.SCValType{identifier: :SCV_LEDGER_KEY_CONTRACT_INSTANCE} + }, + durability: %StellarBase.XDR.ContractDataDurability{identifier: :PERSISTENT}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :EXPIRATION_EXTENSION} + } = + ContractData.new( + contract: contract, + key: key, + durability: durability, + body_type: body_type + ) + |> ContractData.to_xdr() + end + + test "to_xdr/1 with invalid struct" do + {:error, :invalid_struct} = ContractData.to_xdr(%{}) + end +end diff --git a/test/tx_build/ledger_footprint_test.exs b/test/tx_build/ledger_footprint_test.exs new file mode 100644 index 00000000..b01a953d --- /dev/null +++ b/test/tx_build/ledger_footprint_test.exs @@ -0,0 +1,89 @@ +defmodule Stellar.TxBuild.LedgerFootprintTest do + use ExUnit.Case + + alias Stellar.TxBuild.{LedgerFootprint, LedgerKey} + alias StellarBase.XDR.LedgerFootprint, as: LedgerFootprintXDR + + setup do + hash = "ABC123" + body_type = :data_entry + + contract_code_args = [hash: hash, body_type: body_type] + read_only = [LedgerKey.new({:contract_code, contract_code_args})] + + read_write = [ + LedgerKey.new({:contract_code, contract_code_args}), + LedgerKey.new({:contract_code, contract_code_args}) + ] + + ledger_footprint = LedgerFootprint.new(read_only: read_only, read_write: read_write) + + xdr = %LedgerFootprintXDR{ + read_only: %StellarBase.XDR.LedgerKeyList{ + ledger_keys: [ + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + } + ] + }, + read_write: %StellarBase.XDR.LedgerKeyList{ + ledger_keys: [ + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + }, + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + } + ] + } + } + + %{ + read_only: read_only, + read_write: read_write, + ledger_footprint: ledger_footprint, + xdr: xdr + } + end + + test "new/2", %{read_only: read_only, read_write: read_write} do + %LedgerFootprint{read_only: ^read_only, read_write: ^read_write} = + LedgerFootprint.new(read_only: read_only, read_write: read_write) + end + + test "new/2 with default values" do + %LedgerFootprint{read_only: [], read_write: []} = LedgerFootprint.new() + end + + test "new/2 list with invalid_ledger_keys" do + {:error, :invalid_ledger_keys} = LedgerFootprint.new(read_only: [:invalid]) + end + + test "new/2 with invalid_ledger_keys" do + {:error, :invalid_ledger_keys} = LedgerFootprint.new(read_only: :invalid) + end + + test "new/2 with invalid arguments" do + {:error, :invalid_ledger_footprint} = LedgerFootprint.new(:invalid) + end + + test "to_xdr/1", %{ledger_footprint: ledger_footprint, xdr: xdr} do + ^xdr = LedgerFootprint.to_xdr(ledger_footprint) + end + + test "to_xdr/1 invalid struct" do + {:error, :invalid_struct} = LedgerFootprint.to_xdr(:invalid) + end +end diff --git a/test/tx_build/ledger_key_test.exs b/test/tx_build/ledger_key_test.exs index 5c794e78..383ce4b2 100644 --- a/test/tx_build/ledger_key_test.exs +++ b/test/tx_build/ledger_key_test.exs @@ -2,8 +2,18 @@ defmodule Stellar.TxBuild.LedgerKeyTest do use ExUnit.Case alias Stellar.Test.Fixtures.XDR, as: XDRFixtures - alias Stellar.TxBuild.LedgerKey - alias Stellar.TxBuild.Ledger.{Account, ClaimableBalance, Data, LiquidityPool, Offer, Trustline} + alias Stellar.TxBuild.{LedgerKey, SCAddress, SCVal} + + alias Stellar.TxBuild.Ledger.{ + Account, + ClaimableBalance, + Data, + ContractCode, + ContractData, + LiquidityPool, + Offer, + Trustline + } describe "account" do setup do @@ -181,4 +191,81 @@ defmodule Stellar.TxBuild.LedgerKeyTest do test "new/2 invalid_ledger_key" do {:error, :invalid_ledger_key} = LedgerKey.new({:test, [account_id: "ABC"]}) end + + describe "contract_data" do + setup do + contract = SCAddress.new("CCEMOFO5TE7FGOAJOA3RDHPC6RW3CFXRVIGOFQPFE4ZGOKA2QEA636SN") + key = SCVal.new(ledger_key_contract_instance: nil) + durability = :persistent + body_type = :data_entry + + contract_data = + ContractData.new( + contract: contract, + key: key, + durability: durability, + body_type: body_type + ) + + contract_data_args = [ + contract: contract, + key: key, + durability: durability, + body_type: body_type + ] + + %{ + contract_data: contract_data, + contract_data_args: contract_data_args, + xdr: XDRFixtures.ledger_key_contract_data() + } + end + + test "new/2", %{contract_data: contract_data, contract_data_args: contract_data_args} do + %LedgerKey{entry: ^contract_data, type: :contract_data} = + LedgerKey.new({:contract_data, contract_data_args}) + end + + test "new/2 with_invalid_attributes" do + {:error, :invalid_contract_data} = LedgerKey.new({:contract_data, [invalid: "ABC"]}) + end + + test "to_xdr/1", %{contract_data_args: contract_data_args, xdr: xdr} do + ^xdr = + LedgerKey.new({:contract_data, contract_data_args}) + |> LedgerKey.to_xdr() + end + end + + describe "contract_code" do + setup do + hash = "ABC123" + body_type = :data_entry + + contract_code = ContractCode.new(hash: hash, body_type: body_type) + + contract_code_args = [hash: hash, body_type: body_type] + + %{ + contract_code: contract_code, + contract_code_args: contract_code_args, + xdr: XDRFixtures.ledger_key_contract_code() + } + end + + test "new/2", %{contract_code: contract_code, contract_code_args: contract_code_args} do + %LedgerKey{entry: ^contract_code, type: :contract_code} = + LedgerKey.new({:contract_code, contract_code_args}) + end + + test "new/2 with_invalid_attributes" do + {:error, :invalid_contract_code} = LedgerKey.new({:contract_code, [invalid: "ABC"]}) + end + + test "to_xdr/1", %{contract_code_args: contract_code_args, xdr: xdr} do + ^xdr = + LedgerKey.new({:contract_code, contract_code_args}) + |> LedgerKey.to_xdr() + end + end end diff --git a/test/tx_build/soroba_tarsaction_data_test.exs b/test/tx_build/soroba_tarsaction_data_test.exs new file mode 100644 index 00000000..9a0c3d8a --- /dev/null +++ b/test/tx_build/soroba_tarsaction_data_test.exs @@ -0,0 +1,113 @@ +defmodule Stellar.TxBuild.SorobanTransactionDataTest do + use ExUnit.Case + + alias Stellar.TxBuild.{LedgerFootprint, LedgerKey} + alias Stellar.TxBuild.{SorobanResources, SorobanTransactionData} + + setup do + hash = "ABC123" + body_type = :data_entry + + contract_code_args = [hash: hash, body_type: body_type] + read_only = [LedgerKey.new({:contract_code, contract_code_args})] + + read_write = [ + LedgerKey.new({:contract_code, contract_code_args}), + LedgerKey.new({:contract_code, contract_code_args}) + ] + + footprint = LedgerFootprint.new(read_only: read_only, read_write: read_write) + instructions = 1000 + read_bytes = 1024 + write_bytes = 512 + extended_meta_data_size_bytes = 256 + + resources = + SorobanResources.new( + footprint: footprint, + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + ) + + refundable_fee = 100 + + soroban_transaction_data = + SorobanTransactionData.new(resources: resources, refundable_fee: refundable_fee) + + xdr = %StellarBase.XDR.SorobanTransactionData{ + ext: %StellarBase.XDR.ExtensionPoint{ + extension_point: %StellarBase.XDR.Void{value: nil}, + type: 0 + }, + resources: %StellarBase.XDR.SorobanResources{ + footprint: %StellarBase.XDR.LedgerFootprint{ + read_only: %StellarBase.XDR.LedgerKeyList{ + ledger_keys: [ + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + } + ] + }, + read_write: %StellarBase.XDR.LedgerKeyList{ + ledger_keys: [ + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + }, + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + } + ] + } + }, + instructions: %StellarBase.XDR.UInt32{datum: 1000}, + read_bytes: %StellarBase.XDR.UInt32{datum: 1024}, + write_bytes: %StellarBase.XDR.UInt32{datum: 512}, + extended_meta_data_size_bytes: %StellarBase.XDR.UInt32{datum: 256} + }, + refundable_fee: %StellarBase.XDR.Int64{datum: 100} + } + + %{ + resources: resources, + refundable_fee: refundable_fee, + soroban_transaction_data: soroban_transaction_data, + xdr: xdr + } + end + + test "new/1", %{resources: resources, refundable_fee: refundable_fee} do + %SorobanTransactionData{resources: ^resources, refundable_fee: ^refundable_fee} = + SorobanTransactionData.new(resources: resources, refundable_fee: refundable_fee) + end + + test "new/1 with invalid soroban transaction args" do + {:error, :invalid_soroban_transaction_data} = SorobanTransactionData.new(:invalid) + end + + test "new/1 with invalid resources", %{refundable_fee: refundable_fee} do + {:error, :invalid_resources} = + SorobanTransactionData.new(resources: :invalid, refundable_fee: refundable_fee) + end + + test "to_xdr/1", %{soroban_transaction_data: soroban_transaction_data, xdr: xdr} do + ^xdr = SorobanTransactionData.to_xdr(soroban_transaction_data) + end + + test "to_xdr/1 with invalid struct" do + {:error, :invalid_struct} = SorobanTransactionData.to_xdr(%{}) + end +end diff --git a/test/tx_build/soroban_resources_test.exs b/test/tx_build/soroban_resources_test.exs new file mode 100644 index 00000000..0a7b0014 --- /dev/null +++ b/test/tx_build/soroban_resources_test.exs @@ -0,0 +1,147 @@ +defmodule Stellar.TxBuild.SorobanResourcesTest do + use ExUnit.Case + + alias Stellar.TxBuild.{LedgerFootprint, LedgerKey} + + alias Stellar.TxBuild.SorobanResources + + setup do + hash = "ABC123" + body_type = :data_entry + + contract_code_args = [hash: hash, body_type: body_type] + read_only = [LedgerKey.new({:contract_code, contract_code_args})] + + read_write = [ + LedgerKey.new({:contract_code, contract_code_args}), + LedgerKey.new({:contract_code, contract_code_args}) + ] + + footprint = LedgerFootprint.new(read_only: read_only, read_write: read_write) + instructions = 1000 + read_bytes = 1024 + write_bytes = 512 + extended_meta_data_size_bytes = 256 + + %{ + footprint: footprint, + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + } + end + + test "new/1", %{ + footprint: footprint, + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + } do + %SorobanResources{ + footprint: ^footprint, + instructions: ^instructions, + read_bytes: ^read_bytes, + write_bytes: ^write_bytes, + extended_meta_data_size_bytes: ^extended_meta_data_size_bytes + } = + SorobanResources.new( + footprint: footprint, + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + ) + end + + test "new/1 with invalid instructions", %{ + footprint: footprint, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + } do + {:error, :invalid_soroban_resources_args} = + SorobanResources.new( + footprint: footprint, + instructions: "invalid_instructions", + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + ) + end + + test "new/1 with invalid footprint", %{ + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + } do + {:error, :invalid_footprint} = + SorobanResources.new( + footprint: "invalid", + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + ) + end + + test "to_xdr/1", %{ + footprint: footprint, + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + } do + %StellarBase.XDR.SorobanResources{ + footprint: %StellarBase.XDR.LedgerFootprint{ + read_only: %StellarBase.XDR.LedgerKeyList{ + ledger_keys: [ + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + } + ] + }, + read_write: %StellarBase.XDR.LedgerKeyList{ + ledger_keys: [ + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + }, + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.LedgerKeyContractCode{ + hash: %StellarBase.XDR.Hash{value: "ABC123"}, + body_type: %StellarBase.XDR.ContractEntryBodyType{identifier: :DATA_ENTRY} + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + } + ] + } + }, + instructions: %StellarBase.XDR.UInt32{datum: 1000}, + read_bytes: %StellarBase.XDR.UInt32{datum: 1024}, + write_bytes: %StellarBase.XDR.UInt32{datum: 512}, + extended_meta_data_size_bytes: %StellarBase.XDR.UInt32{datum: 256} + } = + SorobanResources.new( + footprint: footprint, + instructions: instructions, + read_bytes: read_bytes, + write_bytes: write_bytes, + extended_meta_data_size_bytes: extended_meta_data_size_bytes + ) + |> SorobanResources.to_xdr() + end + + test "to_xdr/1 with invalid struct" do + {:error, :invalid_struct} = SorobanResources.to_xdr(%{}) + end +end From 5ca94900241e3a6a3d5bddd34846bacb5bcdfd6a Mon Sep 17 00:00:00 2001 From: Miguel Nieto <39246879+miguelnietoa@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:17:16 -0500 Subject: [PATCH 4/6] Update allowed endpoints due to scorecard upgrade (#324) --- .github/workflows/scorecards.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index dfc21ce0..323fe44c 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -31,10 +31,10 @@ jobs: api.osv.dev:443 api.securityscorecards.dev:443 bestpractices.coreinfrastructure.org:443 - fulcio.sigstore.dev:443 + *.sigstore.dev:443 github.com:443 - rekor.sigstore.dev:443 sigstore-tuf-root.storage.googleapis.com:443 + oss-fuzz-build-logs.storage.googleapis.com:443 - name: "Checkout code" uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 From 0ea0d179a9228e79ba6497cfae13c0da9ab3adbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rold=C3=A1n?= <62166813+Odraxs@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:10:22 -0500 Subject: [PATCH 5/6] RestoreFootprint operation (#325) * Restore footprint * Add requested changes --------- Co-authored-by: Edwin Steven Guayacan --- docs/README.md | 1 + .../soroban/bump_footprint_expiration.md | 1 + docs/examples/soroban/restore_footprint.md | 94 +++++++++++++++++++ lib/tx_build/operation.ex | 3 + lib/tx_build/restore_footprint.ex | 38 ++++++++ test/tx_build/restore_footprint_test.exs | 27 ++++++ 6 files changed, 164 insertions(+) create mode 100644 docs/examples/soroban/restore_footprint.md create mode 100644 lib/tx_build/restore_footprint.ex create mode 100644 test/tx_build/restore_footprint_test.exs diff --git a/docs/README.md b/docs/README.md index b3ba17e0..a2e9c15f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,3 +17,4 @@ * [Create Contract](/docs/examples/soroban/create_contract.md) * [Invoke Contract Function](/docs/examples/soroban/invoke_contract_functions.md) * [Bump Footprint Expiration](/docs/examples/soroban/bump_footprint_expiration.md) +* [Restore Footprint](/docs/examples/soroban/restore_footprint.md) diff --git a/docs/examples/soroban/bump_footprint_expiration.md b/docs/examples/soroban/bump_footprint_expiration.md index 3875c7c7..2684f28a 100644 --- a/docs/examples/soroban/bump_footprint_expiration.md +++ b/docs/examples/soroban/bump_footprint_expiration.md @@ -75,6 +75,7 @@ bump_footprint_op = BumpFootprintExpiration.new(ledgers_to_expire: 1000) source_account |> Stellar.TxBuild.new(sequence_number: sequence_number) |> Stellar.TxBuild.add_operation(bump_footprint_op) +|> Stellar.TxBuild.set_soroban_data(soroban_data) |> Stellar.TxBuild.envelope() # Simulate Transaction diff --git a/docs/examples/soroban/restore_footprint.md b/docs/examples/soroban/restore_footprint.md new file mode 100644 index 00000000..66c748a8 --- /dev/null +++ b/docs/examples/soroban/restore_footprint.md @@ -0,0 +1,94 @@ +# Restore Footprint +The `RestoreFootprint` operation is used to restore a contract data entry's. The restored entry will have its expiration ledger bumped to the [minimums](https://github.com/stellar/stellar-core/blob/2109a168a895349f87b502ae3d182380b378fa47/src/ledger/NetworkConfig.h#L77-L78) the network allows for newly created entries, which is 4096 + current ledger for persistent entries, and 16 + current ledger for temporary entries. + +A contract instance, wasm hash, and data storage entry (persistent/instance/temporary) can expire, so in case you need any of these already expired info, you can use this restore for it. +Read more about it: +- https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#restorefootprintop +- https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html + +In this example, we will restore a contract instance of an already expired contract in the network. + +> **Warning** +> Please note that Soroban is still under development, so breaking changes may occur. + +> **Note** +> All this actions require to use `simulateTransaction` and `sendTransaction` RPC endpoints when specified in the code comments to achieve the restore footprint. + +```elixir +alias Stellar.TxBuild.{ + Account, + BaseFee, + LedgerKey, + RestoreFootprint, + SCAddress, + SCVal, + SequenceNumber, + Signature, + SorobanResources, + SorobanTransactionData +} + +alias Stellar.KeyPair + +contract_sc_address = SCAddress.new("CAMGSYINVVL6WP3Q5WPNL7FS4GZP37TWV7MKIRQF5QMYLK3N2SW4P3RC") +key = SCVal.new(ledger_key_contract_instance: nil) + +keypair = + {public_key, _secret} = + Stellar.KeyPair.from_secret_seed("SCAVFA3PI3MJLTQNMXOUNBSEUOSY66YMG3T2KCQKLQBENNVLVKNPV3EK") + +contract_data = + LedgerKey.new( + {:contract_data, + [ + contract: contract_sc_address, + key: key, + durability: :persistent, + body_type: :data_entry + ]} + ) + +footprint = LedgerFootprint.new(read_write: [contract_data]) + +soroban_data = +[ + footprint: footprint, + instructions: 0, + read_bytes: 0, + write_bytes: 0, + extended_meta_data_size_bytes: 0 +] +|> SorobanResources.new() +|> (&SorobanTransactionData.new(resources: &1, refundable_fee: 0)).() +|> SorobanTransactionData.to_xdr() + +source_account = Account.new(public_key) +{:ok, seq_num} = Accounts.fetch_next_sequence_number(public_key) +sequence_number = SequenceNumber.new(seq_num) +signature = Signature.new(keypair) +restore_footprint_op = RestoreFootprint.new() + +# Use this XDR to simulate the transaction and get the soroban_data and min_resource_fee +source_account +|> Stellar.TxBuild.new(sequence_number: sequence_number) +|> Stellar.TxBuild.add_operation(bump_footprint_op) +|> Stellar.TxBuild.set_soroban_data(soroban_data) +|> Stellar.TxBuild.envelope() + +# Simulate Transaction +soroban_data = + "AAAAAAAAAAAAAAABAAAABgAAAAHO/TJVxUVOMxLMlSFIVfYhn4K3jGxh57QYIImRFZhhywAAABQAAAABAAAAAAAAAAAAAAS0AAAEtAAACWgAAAAAAAAB1w==" + +min_resource_fee = 37_351 +fee = BaseFee.new(min_resource_fee + 100) + +# Use the XDR generated here to send it to the futurenet +source_account +|> Stellar.TxBuild.new(sequence_number: sequence_number) +|> Stellar.TxBuild.add_operation(restore_footprint_op) +|> Stellar.TxBuild.set_soroban_data(soroban_data) +|> Stellar.TxBuild.set_base_fee(fee) +|> Stellar.TxBuild.sign(signature) +|> Stellar.TxBuild.envelope() + +``` diff --git a/lib/tx_build/operation.ex b/lib/tx_build/operation.ex index 39537efb..6c5c6fc4 100644 --- a/lib/tx_build/operation.ex +++ b/lib/tx_build/operation.ex @@ -25,6 +25,7 @@ defmodule Stellar.TxBuild.Operation do Payment, PathPaymentStrictSend, PathPaymentStrictReceive, + RestoreFootprint, RevokeSponsorship, SetOptions, SetTrustlineFlags, @@ -60,6 +61,7 @@ defmodule Stellar.TxBuild.Operation do | SetTrustlineFlags.t() | InvokeHostFunction.t() | BumpFootprintExpiration.t() + | RestoreFootprint.t() @type t :: %__MODULE__{body: operation(), source_account: OptionalAccount.t()} @@ -107,6 +109,7 @@ defmodule Stellar.TxBuild.Operation do Payment, PathPaymentStrictReceive, PathPaymentStrictSend, + RestoreFootprint, RevokeSponsorship, SetOptions, SetTrustlineFlags, diff --git a/lib/tx_build/restore_footprint.ex b/lib/tx_build/restore_footprint.ex new file mode 100644 index 00000000..9889e246 --- /dev/null +++ b/lib/tx_build/restore_footprint.ex @@ -0,0 +1,38 @@ +defmodule Stellar.TxBuild.RestoreFootprint do + @moduledoc """ + `RestoreFootprint` struct definition. + """ + alias Stellar.TxBuild.OptionalAccount + + alias StellarBase.XDR.{ + ExtensionPoint, + Operations.RestoreFootprint, + OperationBody, + OperationType, + Void + } + + @behaviour Stellar.TxBuild.XDR + + @type value :: nil + + @type t :: %__MODULE__{value: value(), source_account: OptionalAccount.t()} + + defstruct [:value, :source_account] + + @impl true + def new(value \\ nil, opts \\ []) + def new(_value, _opts), do: %__MODULE__{value: nil, source_account: OptionalAccount.new()} + + @impl true + def to_xdr(%__MODULE__{value: _value}) do + op_type = OperationType.new(:RESTORE_FOOTPRINT) + + Void.new() + |> ExtensionPoint.new(0) + |> RestoreFootprint.new() + |> OperationBody.new(op_type) + end + + def to_xdr(_struct), do: {:error, :invalid_struct} +end diff --git a/test/tx_build/restore_footprint_test.exs b/test/tx_build/restore_footprint_test.exs new file mode 100644 index 00000000..6c9c7753 --- /dev/null +++ b/test/tx_build/restore_footprint_test.exs @@ -0,0 +1,27 @@ +defmodule Stellar.TxBuild.RestoreFootprintTest do + use ExUnit.Case + + alias Stellar.TxBuild.RestoreFootprint + + test "new/2" do + %RestoreFootprint{value: nil} = RestoreFootprint.new() + end + + test "to_xdr/1" do + restore_footprint = RestoreFootprint.new() + + %StellarBase.XDR.OperationBody{ + value: %StellarBase.XDR.Operations.RestoreFootprint{ + ext: %StellarBase.XDR.ExtensionPoint{ + extension_point: %StellarBase.XDR.Void{value: nil}, + type: 0 + } + }, + type: %StellarBase.XDR.OperationType{identifier: :RESTORE_FOOTPRINT} + } = RestoreFootprint.to_xdr(restore_footprint) + end + + test "to_xdr/1 with invalid struct" do + {:error, :invalid_struct} = RestoreFootprint.to_xdr(:invalid) + end +end From 78b10c25097a6993686c0e42dc18da8efb9d96c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rold=C3=A1n?= <62166813+Odraxs@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:17:23 -0500 Subject: [PATCH 6/6] Prepare release v0.17.0 (#326) * Prepare release v0.17.0 * Change indentation to two spaces --- CHANGELOG.md | 11 ++++++++++- README.md | 2 +- mix.exs | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51384755..01274495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog -## 0.16.0 (27.07.2023) +## 0.17.0 (04.08.2023) + +* Finish Soroban Preview 10 Support: + * Support authorized invocation with different accounts. + * Support Upload Contract WASM. + * Support Create Contract. + * Support RestoreFootprint operation. + * Support BumpFootprintExpiration operation. + +## 0.16.1 (27.07.2023) * Update stellar base dependency. diff --git a/README.md b/README.md index 1b7418a6..44a93f27 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The **Stellar SDK** is composed of two complementary components: **`TxBuild`** + ```elixir def deps do [ - {:stellar_sdk, "~> 0.16.1"} + {:stellar_sdk, "~> 0.17.0"} ] end ``` diff --git a/mix.exs b/mix.exs index 511b0ca8..3e013d04 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule Stellar.MixProject do use Mix.Project @github_url "https://github.com/kommitters/stellar_sdk" - @version "0.16.1" + @version "0.17.0" def project do [