diff --git a/CHANGELOG.md b/CHANGELOG.md index 923d3493..2fcfd535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.14.0 (08.05.2023) + +* Add `sign_xdr` function to `ContractAuth` module to allow signing base 64 contract auth structures. +* Update `InvokeHostFunction` module to allow adding base 64 authorizations. + ## 0.13.1 (02.05.2023) * Update `SCAddress` module to infer address types: account or contract. * Update all dependencies diff --git a/README.md b/README.md index 0b941ad4..c6150ef2 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.13.1"} + {:stellar_sdk, "~> 0.14.0"} ] end ``` diff --git a/lib/tx_build/contract_auth.ex b/lib/tx_build/contract_auth.ex index 7ed3ad48..f14dd603 100644 --- a/lib/tx_build/contract_auth.ex +++ b/lib/tx_build/contract_auth.ex @@ -17,8 +17,10 @@ defmodule Stellar.TxBuild.ContractAuth do } alias Stellar.{KeyPair, Network} - alias StellarBase.XDR.{ContractAuth, SCVec} + alias StellarBase.XDR.{ContractAuth, EnvelopeType, SCVec} alias StellarBase.XDR.HashIDPreimage, as: HashIDPreimageXDR + alias StellarBase.XDR.Hash, as: HashXDR + alias StellarBase.XDR.HashIDPreimageContractAuth, as: HashIDPreimageContractAuthXDR alias Stellar.TxBuild.{ AddressWithNonce, @@ -132,6 +134,64 @@ defmodule Stellar.TxBuild.ContractAuth do def sign(_args, _val), do: {:error, :invalid_secret_key} + @spec sign_xdr(base_64 :: binary(), secret_key :: binary()) :: binary() + def sign_xdr(base_64, 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() + + {%ContractAuth{ + address_with_nonce: %{ + address_with_nonce: %{ + nonce: nonce + } + }, + authorized_invocation: authorized_invocation + } = contract_auth, + ""} = + base_64 + |> Base.decode64!() + |> ContractAuth.decode_xdr!() + + envelope_type = EnvelopeType.new(:ENVELOPE_TYPE_CONTRACT_AUTH) + + signature = + network_id + |> HashXDR.new() + |> HashIDPreimageContractAuthXDR.new(nonce, authorized_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) + ) + + signature_args = [SCVal.new(map: [public_key_map_entry, signature_map_entry])] + + signature_xdr_val = + [vec: signature_args] + |> SCVal.new() + |> SCVal.to_xdr() + |> (&SCVec.new([&1])).() + + %{ + contract_auth + | signature_args: signature_xdr_val + } + |> ContractAuth.encode_xdr!() + |> Base.encode64() + end + @spec network_id_xdr :: binary() defp network_id_xdr, do: hash(Network.passphrase()) diff --git a/lib/tx_build/invoke_host_function.ex b/lib/tx_build/invoke_host_function.ex index f7a23f54..d7ba5b78 100644 --- a/lib/tx_build/invoke_host_function.ex +++ b/lib/tx_build/invoke_host_function.ex @@ -11,6 +11,7 @@ defmodule Stellar.TxBuild.InvokeHostFunction do alias Stellar.TxBuild.{ContractAuth, HostFunction, OptionalAccount} alias StellarBase.XDR.Operations.InvokeHostFunction + alias StellarBase.XDR.ContractAuth, as: ContractAuthXDR alias StellarBase.XDR.{ OperationBody, @@ -27,7 +28,7 @@ defmodule Stellar.TxBuild.InvokeHostFunction do @type t :: %__MODULE__{ function: HostFunction.t(), footprint: String.t(), - auth: list(ContractAuth.t()), + auth: list(ContractAuth.t()) | String.t(), source_account: OptionalAccount.t() } @@ -43,7 +44,7 @@ defmodule Stellar.TxBuild.InvokeHostFunction do source_account = Keyword.get(args, :source_account) with {:ok, function} <- validate_function(function), - {:ok, footprint} <- validate_footprint({:footprint, footprint}), + {:ok, footprint} <- validate_xdr_string({:xdr, footprint}), {:ok, source_account} <- validate_optional_account({:source_account, source_account}), {:ok, auth} <- validate_auth(auth) do %__MODULE__{ @@ -110,6 +111,32 @@ defmodule Stellar.TxBuild.InvokeHostFunction do |> OperationBody.new(op_type) end + def to_xdr(%__MODULE__{ + function: function, + footprint: footprint, + auth: auth + }) + when is_binary(auth) do + op_type = OperationType.new(:INVOKE_HOST_FUNCTION) + host_function = HostFunction.to_xdr(function) + + {ledger_footprint, _} = + footprint + |> Base.decode64!() + |> LedgerFootprint.decode_xdr!() + + {contract_auth, ""} = + auth + |> Base.decode64!() + |> ContractAuthXDR.decode_xdr!() + + contract_auth_list = ContractAuthList.new([contract_auth]) + + host_function + |> InvokeHostFunction.new(ledger_footprint, contract_auth_list) + |> OperationBody.new(op_type) + end + def to_xdr(%__MODULE__{ function: function, footprint: footprint, @@ -132,26 +159,33 @@ defmodule Stellar.TxBuild.InvokeHostFunction do @spec set_footprint(module :: t(), footprint :: String.t()) :: t() def set_footprint(%__MODULE__{} = module, footprint) do - with {:ok, footprint} <- validate_footprint({:footprint, footprint}) do + with {:ok, footprint} <- validate_xdr_string({:xdr, footprint}) do %{module | footprint: footprint} end end + @spec set_contract_auth(module :: t(), auth :: String.t()) :: t() + def set_contract_auth(%__MODULE__{} = module, auth) when is_binary(auth) do + with {:ok, auth} <- validate_xdr_string({:xdr, auth}) do + %{module | auth: auth} + end + end + @spec validate_function(function :: HostFunction.t()) :: validation() defp validate_function(%HostFunction{} = function), do: {:ok, function} defp validate_function(_), do: {:error, :invalid_function} - @spec validate_footprint(tuple :: tuple()) :: validation() - defp validate_footprint({:footprint, nil}), do: {:ok, nil} + @spec validate_xdr_string(tuple :: tuple()) :: validation() + defp validate_xdr_string({:xdr, nil}), do: {:ok, nil} - defp validate_footprint({:footprint, footprint}) when is_binary(footprint) do - case Base.decode64(footprint) do - {:ok, _} -> {:ok, footprint} - :error -> {:error, :invalid_footprint} + defp validate_xdr_string({:xdr, xdr}) when is_binary(xdr) do + case Base.decode64(xdr) do + {:ok, _} -> {:ok, xdr} + :error -> {:error, :invalid_xdr} end end - defp validate_footprint({:footprint, _}), do: {:error, :invalid_footprint} + defp validate_xdr_string({:xdr, _}), do: {:error, :invalid_xdr} @spec validate_auth(function :: list(ContractAuth.t())) :: validation() defp validate_auth([%ContractAuth{} | _] = auth), do: {:ok, auth} diff --git a/mix.exs b/mix.exs index 7b8d460e..2cd91626 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.13.1" + @version "0.14.0" def project do [ diff --git a/test/support/xdr_fixtures.ex b/test/support/xdr_fixtures.ex index fd323182..899bf0f0 100644 --- a/test/support/xdr_fixtures.ex +++ b/test/support/xdr_fixtures.ex @@ -814,6 +814,197 @@ defmodule Stellar.Test.XDRFixtures do } end + @spec invoke_host_function_auth_operation() :: OperationBody.t() + def invoke_host_function_auth_operation do + %StellarBase.XDR.OperationBody{ + operation: %StellarBase.XDR.Operations.InvokeHostFunction{ + host_function: %StellarBase.XDR.HostFunction{ + host_function: %StellarBase.XDR.SCVec{ + sc_vals: [ + %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.SCBytes{ + value: + <<4, 97, 22, 140, 187, 174, 13, 169, 108, 84, 59, 113, 253, 87, 26, 236, 75, + 68, 84, 157, 80, 63, 154, 249, 231, 104, 92, 206, 219, 193, 97, 60>> + }, + type: %StellarBase.XDR.SCValType{identifier: :SCV_BYTES} + }, + %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.SCSymbol{value: "hello"}, + type: %StellarBase.XDR.SCValType{identifier: :SCV_SYMBOL} + }, + %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.SCSymbol{value: "world"}, + type: %StellarBase.XDR.SCValType{identifier: :SCV_SYMBOL} + } + ] + }, + type: %StellarBase.XDR.HostFunctionType{ + identifier: :HOST_FUNCTION_TYPE_INVOKE_CONTRACT + } + }, + footprint: %StellarBase.XDR.LedgerFootprint{ + read_only: %StellarBase.XDR.LedgerKeyList{ + ledger_keys: [ + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.ContractData{ + contract_id: %StellarBase.XDR.Hash{ + value: + <<4, 97, 22, 140, 187, 174, 13, 169, 108, 84, 59, 113, 253, 87, 26, 236, 75, + 68, 84, 157, 80, 63, 154, 249, 231, 104, 92, 206, 219, 193, 97, 60>> + }, + key: %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.UInt32{datum: 3}, + type: %StellarBase.XDR.SCValType{identifier: :SCV_U32} + } + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_DATA} + }, + %StellarBase.XDR.LedgerKey{ + entry: %StellarBase.XDR.ContractCode{ + hash: %StellarBase.XDR.Hash{ + value: + <<125, 247, 111, 172, 116, 80, 165, 58, 211, 175, 124, 213, 91, 23, 88, 64, + 110, 146, 161, 223, 254, 135, 195, 227, 132, 236, 7, 143, 10, 140, 164, + 75>> + } + }, + type: %StellarBase.XDR.LedgerEntryType{identifier: :CONTRACT_CODE} + } + ] + }, + read_write: %StellarBase.XDR.LedgerKeyList{ledger_keys: []} + }, + auth: %StellarBase.XDR.ContractAuthList{ + auth: [ + %StellarBase.XDR.ContractAuth{ + address_with_nonce: %StellarBase.XDR.OptionalAddressWithNonce{ + address_with_nonce: %StellarBase.XDR.AddressWithNonce{ + address: %StellarBase.XDR.SCAddress{ + sc_address: %StellarBase.XDR.AccountID{ + account_id: %StellarBase.XDR.PublicKey{ + public_key: %StellarBase.XDR.UInt256{ + datum: + <<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>> + }, + type: %StellarBase.XDR.PublicKeyType{ + identifier: :PUBLIC_KEY_TYPE_ED25519 + } + } + }, + type: %StellarBase.XDR.SCAddressType{ + identifier: :SC_ADDRESS_TYPE_ACCOUNT + } + }, + nonce: %StellarBase.XDR.UInt64{datum: 4} + } + }, + authorized_invocation: %StellarBase.XDR.AuthorizedInvocation{ + contract_id: %StellarBase.XDR.Hash{ + value: + <<190, 65, 56, 179, 28, 197, 208, 217, 217, 27, 83, 25, 61, 116, 49, 109, 37, + 68, 6, 121, 78, 192, 248, 29, 62, 212, 15, 77, 193, 184, 106, 110>> + }, + function_name: %StellarBase.XDR.SCSymbol{value: "swap"}, + args: %StellarBase.XDR.SCVec{ + sc_vals: [ + %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.Int128Parts{ + lo: %StellarBase.XDR.UInt64{datum: 100}, + hi: %StellarBase.XDR.UInt64{datum: 0} + }, + type: %StellarBase.XDR.SCValType{identifier: :SCV_I128} + }, + %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.Int128Parts{ + lo: %StellarBase.XDR.UInt64{datum: 5000}, + hi: %StellarBase.XDR.UInt64{datum: 0} + }, + type: %StellarBase.XDR.SCValType{identifier: :SCV_I128} + } + ] + }, + sub_invocations: %StellarBase.XDR.AuthorizedInvocationList{ + sub_invocations: [] + } + }, + signature_args: %StellarBase.XDR.SCVec{ + sc_vals: [ + %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.OptionalSCVec{ + sc_vec: %StellarBase.XDR.SCVec{ + sc_vals: [ + %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.OptionalSCMap{ + sc_map: %StellarBase.XDR.SCMap{ + scmap_entries: [ + %StellarBase.XDR.SCMapEntry{ + key: %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.SCSymbol{ + value: "public_key" + }, + type: %StellarBase.XDR.SCValType{ + identifier: :SCV_SYMBOL + } + }, + val: %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.SCBytes{ + 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>> + }, + type: %StellarBase.XDR.SCValType{ + identifier: :SCV_BYTES + } + } + }, + %StellarBase.XDR.SCMapEntry{ + key: %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.SCSymbol{ + value: "signature" + }, + type: %StellarBase.XDR.SCValType{ + identifier: :SCV_SYMBOL + } + }, + val: %StellarBase.XDR.SCVal{ + value: %StellarBase.XDR.SCBytes{ + value: + <<149, 29, 23, 78, 150, 73, 24, 83, 225, 210, 53, 91, + 131, 161, 84, 76, 163, 55, 121, 78, 9, 50, 5, 112, + 176, 230, 23, 225, 117, 119, 139, 92, 250, 13, 94, + 246, 196, 102, 140, 59, 179, 215, 139, 99, 251, 5, + 196, 221, 87, 137, 198, 9, 254, 247, 128, 125, 216, + 14, 159, 102, 103, 153, 18, 2>> + }, + type: %StellarBase.XDR.SCValType{ + identifier: :SCV_BYTES + } + } + } + ] + } + }, + type: %StellarBase.XDR.SCValType{identifier: :SCV_MAP} + } + ] + } + }, + type: %StellarBase.XDR.SCValType{identifier: :SCV_VEC} + } + ] + } + } + ] + } + }, + type: %StellarBase.XDR.OperationType{identifier: :INVOKE_HOST_FUNCTION} + } + end + @spec invoke_host_function_op_xdr( function :: HostFunction.t(), footprint :: String.t(), diff --git a/test/tx_build/contract_auth_test.exs b/test/tx_build/contract_auth_test.exs index 119759ea..dfe739bf 100644 --- a/test/tx_build/contract_auth_test.exs +++ b/test/tx_build/contract_auth_test.exs @@ -61,11 +61,22 @@ defmodule Stellar.TxBuild.ContractAuthTest do # SCVAL signature_args = [TxSCVal.new(i32: 456)] + secret_key = "SCAVFA3PI3MJLTQNMXOUNBSEUOSY66YMG3T2KCQKLQBENNVLVKNPV3EK" + + base_64_without_sign = + "AAAAAQAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftYAAAAAAAAABL5BOLMcxdDZ2RtTGT10MW0lRAZ5TsD4HT7UD03BuGpuAAAABHN3YXAAAAACAAAACgAAAAAAAABkAAAAAAAAAAAAAAAKAAAAAAAAE4gAAAAAAAAAAAAAAAAAAAAA" + + base_64_string = + "AAAAAQAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftYAAAAAAAAABL5BOLMcxdDZ2RtTGT10MW0lRAZ5TsD4HT7UD03BuGpuAAAABHN3YXAAAAACAAAACgAAAAAAAABkAAAAAAAAAAAAAAAKAAAAAAAAE4gAAAAAAAAAAAAAAAAAAAABAAAAEAAAAAEAAAABAAAAEQAAAAEAAAACAAAADwAAAApwdWJsaWNfa2V5AAAAAAANAAAAIMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WAAAADwAAAAlzaWduYXR1cmUAAAAAAAANAAAAQJUdF06WSRhT4dI1W4OhVEyjN3lOCTIFcLDmF+F1d4tc+g1e9sRmjDuz14tj+wXE3VeJxgn+94B92A6fZmeZEgI=" + %{ address_with_nonce: address_with_nonce, authorized_invocation: authorized_invocation_2, signature_args: signature_args, - optional_address_with_nonce: optional_address_with_nonce + optional_address_with_nonce: optional_address_with_nonce, + secret_key: secret_key, + base_64_string: base_64_string, + base_64_without_sign: base_64_without_sign } end @@ -285,4 +296,12 @@ defmodule Stellar.TxBuild.ContractAuthTest do test "to_xdr/1 when the struct is invalid" do {:error, :invalid_struct_contract_auth} = TxContractAuth.to_xdr("invalid_struct") end + + test "sign_xdr/2", %{ + base_64_without_sign: base_64_without_sign, + base_64_string: base_64_string, + secret_key: secret_key + } do + ^base_64_string = TxContractAuth.sign_xdr(base_64_without_sign, secret_key) + end end diff --git a/test/tx_build/invoke_host_function_test.exs b/test/tx_build/invoke_host_function_test.exs index f3812ba0..23efc3ed 100644 --- a/test/tx_build/invoke_host_function_test.exs +++ b/test/tx_build/invoke_host_function_test.exs @@ -15,7 +15,8 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do only: [ invoke_host_function_op_xdr: 1, invoke_host_function_op_xdr: 2, - invoke_host_function_op_xdr: 3 + invoke_host_function_op_xdr: 3, + invoke_host_function_auth_operation: 0 ] describe "InvokeHostFunction" do @@ -63,6 +64,9 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do args: args ) + contract_auth = + "AAAAAQAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftYAAAAAAAAABL5BOLMcxdDZ2RtTGT10MW0lRAZ5TsD4HT7UD03BuGpuAAAABHN3YXAAAAACAAAACgAAAAAAAABkAAAAAAAAAAAAAAAKAAAAAAAAE4gAAAAAAAAAAAAAAAAAAAABAAAAEAAAAAEAAAABAAAAEQAAAAEAAAACAAAADwAAAApwdWJsaWNfa2V5AAAAAAANAAAAIMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WAAAADwAAAAlzaWduYXR1cmUAAAAAAAANAAAAQJUdF06WSRhT4dI1W4OhVEyjN3lOCTIFcLDmF+F1d4tc+g1e9sRmjDuz14tj+wXE3VeJxgn+94B92A6fZmeZEgI=" + %{ function: function, footprint: footprint, @@ -70,7 +74,9 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do xdr: invoke_host_function_op_xdr(function, footprint), xdr_with_auth: invoke_host_function_op_xdr(function, footprint, [contract_authentication]), - xdr_without_auth_and_footprint: invoke_host_function_op_xdr(function) + xdr_without_auth_and_footprint: invoke_host_function_op_xdr(function), + invoke_host_function_auth_operation: invoke_host_function_auth_operation(), + contract_auth: contract_auth } end @@ -132,7 +138,7 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do end test "new/2 with_invalid_footprint", %{function: function} do - {:error, :invalid_footprint} = InvokeHostFunction.new(function: function, footprint: 11) + {:error, :invalid_xdr} = InvokeHostFunction.new(function: function, footprint: 11) end test "new/2 with invalid operation attribute" do @@ -151,10 +157,27 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do end test "set_footprint with invalid footprint", %{function: function} do - {:error, :invalid_footprint} = + {:error, :invalid_xdr} = [function: function] |> InvokeHostFunction.new() - |> InvokeHostFunction.set_footprint("invalid_footprint") + |> InvokeHostFunction.set_footprint("invalid_xdr") + end + + test "set_contract_auth", %{function: function, contract_auth: contract_auth} do + %InvokeHostFunction{ + function: ^function, + auth: ^contract_auth + } = + [function: function] + |> InvokeHostFunction.new() + |> InvokeHostFunction.set_contract_auth(contract_auth) + end + + test "set_contract_auth with invalid contract_auth", %{function: function} do + {:error, :invalid_xdr} = + [function: function] + |> InvokeHostFunction.new() + |> InvokeHostFunction.set_contract_auth("invalid_xdr") end test "to_xdr/1", %{function: function, footprint: footprint, xdr: xdr} do @@ -185,5 +208,18 @@ defmodule Stellar.TxBuild.InvokeHostFunctionTest do |> InvokeHostFunction.new() |> InvokeHostFunction.to_xdr() end + + test "to_xdr/1 with footprint and xdr authorization", %{ + function: function, + invoke_host_function_auth_operation: xdr, + contract_auth: contract_auth, + footprint: footprint + } do + ^xdr = + [function: function, footprint: footprint] + |> InvokeHostFunction.new() + |> InvokeHostFunction.set_contract_auth(contract_auth) + |> InvokeHostFunction.to_xdr() + end end end