Repository: StellarCN/py-stellar-base

Issue:
Soroban - Failing to invoke function with struct parameter
```
stellar-sdk==9.0.0b0
```

I deployed [this contract](https://github.com/bp-ventures/lightecho-stellar-oracle/blob/328e897bb69ea194d642540584bb4c94362bbc8e/oracle-onchain/sep40/contract/src/contract.rs) which has this function inside it:

```
fn add_prices(env: Env, prices: Vec<Price>);
```

When [invoking the function via Rust](https://github.com/bp-ventures/lightecho-stellar-oracle/blob/328e897bb69ea194d642540584bb4c94362bbc8e/oracle-onchain/sep40/contract/src/test.rs#L578), it works as expected.

But when invoking it from Python, I get this error:
```
0: [Diagnostic Event] topics:[error, Error(Value, InternalError)], data:"failed to convert ScVal to host value
```


## How to reproduce

`test.py`:
```
from stellar_sdk import scval, xdr as stellar_xdr
from stellar_sdk.soroban_server import SorobanServer
from stellar_sdk import TransactionBuilder, Keypair
import time
from stellar_sdk.exceptions import PrepareTransactionException
from stellar_sdk.soroban_rpc import GetTransactionStatus, SendTransactionStatus

SOURCE_SECRET = "SDFWYGBNP5TW4MS7RY5D4FILT65R2IEPWGL34NY2TLSU4DC4BJNXUAMU"
CONTRACT_ID = "CA3BDS2ME2F4SFMRWWHITPQXQL5DDA35SEVOETK7BDDEJRAJQJDFUSDO"
NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"
RPC_URL = "https://soroban-testnet.stellar.org:443/"
BASE_FEE = 300000

price_list = []
price_struct = scval.to_struct(
    {
        "source": scval.to_uint32(999),
        "asset": scval.to_enum("Other", scval.to_symbol("TEST")),
        "price": scval.to_int128(1),
        "timestamp": scval.to_uint64(1699779449),
    }
)
price_list.append(price_struct)
price_vec = scval.to_vec(price_list)
parameters = [price_vec]

server = SorobanServer(RPC_URL)
source_kp = Keypair.from_secret(SOURCE_SECRET)
source_account = server.load_account(source_kp.public_key)
tx = (
    TransactionBuilder(
        source_account,
        NETWORK_PASSPHRASE,
        base_fee=BASE_FEE,
    )
    .set_timeout(30)
    .append_invoke_contract_function_op(
        CONTRACT_ID,
        "add_prices",
        parameters,
    )
    .build()
)


try:
    tx = server.prepare_transaction(tx)
except PrepareTransactionException as e:
    print(e.simulate_transaction_response)
    raise

tx.sign(source_kp)
send_transaction_data = server.send_transaction(tx)
if send_transaction_data.status != SendTransactionStatus.PENDING:
    raise RuntimeError(f"Failed to send transaction: {send_transaction_data}")
tx_hash = send_transaction_data.hash

while True:
    get_transaction_data = server.get_transaction(tx_hash)
    if get_transaction_data.status != GetTransactionStatus.NOT_FOUND:
        break
    time.sleep(3)
tx_data = get_transaction_data

if tx_data.status != GetTransactionStatus.SUCCESS:
    raise RuntimeError(f"Failed to send transaction: {tx_data}")

print(tx_data)
```

```
python test.py
```

```
preparing xdr:
AAAAAgAAAADc5Y9UXuU88Ks7OirvJZTwjhMZOQhkC4Y6+CTwK5Sw1gAEk+AAGSgAAAAABwAAAAEAAAAAAAAAAAAAAABlUJoIAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABNhHLTCaLyRWRtY6JvheC+jGDfZEq4k1fCMZExAmCRloAAAAKYWRkX3ByaWNlcwAAAAAAAQAAABAAAAABAAAAAQAAABEAAAABAAAABAAAAA8AAAAGc291cmNlAAAAAAADAAAD5wAAAA8AAAAFYXNzZXQAAAAAAAAQAAAAAQAAAAIAAAAPAAAABU90aGVyAAAAAAAADwAAAARURVNUAAAADwAAAAVwcmljZQAAAAAAAAoAAAAAAAAAAAAAAAAAAAABAAAADwAAAAl0aW1lc3RhbXAAAAAAAAAFAAAAAGVQk3kAAAAAAAAAAAAAAAA=
error='host invocation failed\n\nCaused by:\n    HostError: Error(Value, InternalError)\n    \n    Event log (newest first):\n       0: [Diagnostic Event] topics:[error, Error(Value, InternalError)], data:"failed to convert ScVal to host value"\n       1: [Diagnostic Event] topics:[error, Error(Value, InternalError)], data:"failed to convert ScVal to host value"\n    \n    Backtrace (newest first):\n       0: <core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::try_fold\n       1: <alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter\n       2: soroban_env_host::host::metered_clone::MeteredIterator::metered_collect\n       3: soroban_env_host::host::frame::<impl soroban_env_host::host::Host>::invoke_function\n       4: preflight::preflight::preflight_invoke_hf_op\n       5: preflight::preflight_invoke_hf_op::{{closure}}\n       6: core::ops::function::FnOnce::call_once{{vtable.shim}}\n       7: preflight::catch_preflight_panic\n       8: _cgo_0b49d6ed4a0b_Cfunc_preflight_invoke_hf_op\n                 at tmp/go-build/cgo-gcc-prolog:103:11\n       9: runtime.asmcgocall\n                 at ./runtime/asm_amd64.s:848\n    \n    ' transaction_data=None min_resource_fee=None events=['AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAVlcnJvcgAAAAAAAAIAAAAIAAAABwAAAA4AAAAlZmFpbGVkIHRvIGNvbnZlcnQgU2NWYWwgdG8gaG9zdCB2YWx1ZQAAAA==', 'AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAVlcnJvcgAAAAAAAAIAAAAIAAAABwAAAA4AAAAlZmFpbGVkIHRvIGNvbnZlcnQgU2NWYWwgdG8gaG9zdCB2YWx1ZQAAAA=='] results=None cost=SimulateTransactionCost(cpu_insns=0, mem_bytes=0) restore_preamble=None latest_ledger=2481914
Traceback (most recent call last):
  File "/home/yuri/test.py", line 47, in <module>
    tx = server.prepare_transaction(tx)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/yuri/.cache/pypoetry/virtualenvs/cli-sOWdBO30-py3.11/lib/python3.11/site-packages/stellar_sdk/soroban_server.py", line 310, in prepare_transaction
    raise PrepareTransactionException(
stellar_sdk.exceptions.PrepareTransactionException: Simulation transaction failed, the response contains error information.
```

I looked into the XDR and it looks correct, the parameter is a ScVec and each item is a ScMap, which should get converted into Struct but I guess that is the part that's broken.
Not sure if the issue is coming from Soroban or the Python SDK.

Comments:

Patch:
diff --git a/stellar_sdk/scval.py b/stellar_sdk/scval.py
index 418a6d5e..2a3c9dbb 100644
--- a/stellar_sdk/scval.py
+++ b/stellar_sdk/scval.py
@@ -624,8 +624,11 @@ def to_struct(data: Dict[str, stellar_xdr.SCVal]) -> stellar_xdr.SCVal:
     :param data: The dict value to convert.
     :return: A new :class:`stellar_sdk.xdr.SCVal` XDR object.
     """
+    # sort the dict by key to ensure the order of the fields.
+    # see https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-01.md#validity
+    sorted_data = dict(sorted(data.items()))
     v = dict()
-    for key, val in data.items():
+    for key, val in sorted_data.items():
         v[to_symbol(key)] = val
     return to_map(v)
 


Test patch:
diff --git a/tests/test_scval.py b/tests/test_scval.py
index 73c15508..a091c7aa 100644
--- a/tests/test_scval.py
+++ b/tests/test_scval.py
@@ -315,14 +315,14 @@ def test_tuple_struct():
 
 def test_struct():
     v = {
-        "data1": to_int32(1),
-        "data2": to_int256(23423432),
-        "data3": to_string("world"),
-        "data4": to_vec([to_int32(1), to_int256(23423432), to_string("world")]),
-        "data5": to_struct(
+        "simpleData": to_int32(1),
+        "a": to_int256(23423432),
+        "data": to_string("world"),
+        "a1": to_vec([to_int32(1), to_int256(23423432), to_string("world")]),
+        "A": to_struct(
             {
-                "inner_data1": to_int32(1),
-                "inner_data2": to_int256(23423432),
+                "inner_data2": to_int32(1),
+                "inner_data1": to_int256(23423432),
             }
         ),
     }
@@ -331,22 +331,22 @@ def test_struct():
         stellar_xdr.SCValType.SCV_MAP,
         map=xdr.SCMap(
             [
-                xdr.SCMapEntry(to_symbol("data1"), to_int32(1)),
-                xdr.SCMapEntry(to_symbol("data2"), to_int256(23423432)),
-                xdr.SCMapEntry(to_symbol("data3"), to_string("world")),
                 xdr.SCMapEntry(
-                    to_symbol("data4"),
-                    to_vec([to_int32(1), to_int256(23423432), to_string("world")]),
-                ),
-                xdr.SCMapEntry(
-                    to_symbol("data5"),
+                    to_symbol("A"),
                     to_map(
                         {
-                            to_symbol("inner_data1"): to_int32(1),
-                            to_symbol("inner_data2"): to_int256(23423432),
+                            to_symbol("inner_data1"): to_int256(23423432),
+                            to_symbol("inner_data2"): to_int32(1),
                         }
                     ),
                 ),
+                xdr.SCMapEntry(to_symbol("a"), to_int256(23423432)),
+                xdr.SCMapEntry(
+                    to_symbol("a1"),
+                    to_vec([to_int32(1), to_int256(23423432), to_string("world")]),
+                ),
+                xdr.SCMapEntry(to_symbol("data"), to_string("world")),
+                xdr.SCMapEntry(to_symbol("simpleData"), to_int32(1)),
             ]
         ),
     )