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.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN) Report
> Merging [#817](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN) (2af1285) into [main](https://app.codecov.io/gh/StellarCN/py-stellar-base/commit/32c9801ae8efb426ff3a99c3eca36be18ca0b6a6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN) (32c9801) will **decrease** coverage by `0.01%`.
> Report is 11 commits behind head on main.
> The diff coverage is `95.18%`.

> :exclamation: Current head 2af1285 differs from pull request most recent head a576ac0. Consider uploading reports for the commit a576ac0 to get more accurate results

[![Impacted file tree graph](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817/graphs/tree.svg?width=650&height=150&src=pr&token=kHAKcotfqv&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN)](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN)

```diff
@@            Coverage Diff             @@
##             main     #817      +/-   ##
==========================================
- Coverage   98.63%   98.62%   -0.01%     
==========================================
  Files         253      253              
  Lines       13038    13193     +155     
==========================================
+ Hits        12860    13012     +152     
- Misses        178      181       +3     
```

| [Flag](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN) | `98.62% <95.18%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN) | Coverage Δ | |
|---|---|---|
| [stellar\_sdk/base\_soroban\_server.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvYmFzZV9zb3JvYmFuX3NlcnZlci5weQ==) | `93.75% <ø> (ø)` | |
| [stellar\_sdk/operation/\_\_init\_\_.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvb3BlcmF0aW9uL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [stellar\_sdk/operation/extend\_footprint\_ttl.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvb3BlcmF0aW9uL2V4dGVuZF9mb290cHJpbnRfdHRsLnB5) | `100.00% <100.00%> (ø)` | |
| [stellar\_sdk/scval.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvc2N2YWwucHk=) | `90.52% <100.00%> (+0.05%)` | :arrow_up: |
| [stellar\_sdk/soroban\_data\_builder.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvc29yb2Jhbl9kYXRhX2J1aWxkZXIucHk=) | `100.00% <100.00%> (ø)` | |
| [stellar\_sdk/soroban\_rpc.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvc29yb2Jhbl9ycGMucHk=) | `100.00% <100.00%> (ø)` | |
| [stellar\_sdk/soroban\_server.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvc29yb2Jhbl9zZXJ2ZXIucHk=) | `94.44% <ø> (ø)` | |
| [stellar\_sdk/soroban\_server\_async.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvc29yb2Jhbl9zZXJ2ZXJfYXN5bmMucHk=) | `97.77% <ø> (ø)` | |
| [stellar\_sdk/transaction.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvdHJhbnNhY3Rpb24ucHk=) | `97.11% <100.00%> (ø)` | |
| [stellar\_sdk/transaction\_builder.py](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN#diff-c3RlbGxhcl9zZGsvdHJhbnNhY3Rpb25fYnVpbGRlci5weQ==) | `97.17% <100.00%> (ø)` | |
| ... and [7 more](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN) | |

... and [56 files with indirect coverage changes](https://app.codecov.io/gh/StellarCN/py-stellar-base/pull/817/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=StellarCN)


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)),
             ]
         ),
     )