Repository: StellarCN/py-stellar-base (language: python)

Issue:
Protocol 21 SDK Support
## Protocol 21 SDK Support

Once voted in, the release of Protocol 21 will introduce following new CAPs:

- [CAP-51](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md): Smart Contract Host Functionality
- [CAP-53](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0053.md): Separate host functions to extend the TTL for contract instance and contract code
- [CAP-54](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0054.md): Soroban refined VM instantiation cost model
- [CAP-55 ](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0055.md): Soroban streamlined linking
- [CAP-56](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0056.md): Soroban intra-transaction module caching


### **XDR changes:**

XDR update [issue details](https://github.com/stellar/stellar-xdr/pull/184) - https://github.com/stellar/stellar-xdr/commit/1a04392432dacc0092caaeae22a600ea1af3c6a5

Reference Implementations:
js-sdk - https://github.com/stellar/js-stellar-base/pull/738


### **Soroban API Changes:**

#### Breaking Changes:

- simulateTransaction: https://github.com/stellar/soroban-rpc/issues/13

#### Non Breaking changes:

-  Add StateDiff to simulateTransaction Response: https://github.com/stellar/soroban-rpc/pull/120

New optional field in `SimulateTransactionResponse` which indicates how the state (ledger entries) will change as a result of the transaction execution.

```
StateDiff       []LedgerEntryDiff            `json:"stateDiff,omitempty"`   

type LedgerEntryDiff struct {
	Before string `json:"before,omitempty"` // LedgerEntry XDR in base64
	After  string `json:"after,omitempty"`  // LedgerEntry XDR in base64
}
```


- Add ledger Range to getHealth RPC: https://github.com/stellar/soroban-rpc/pull/133

New Ledger Range and Ledger Retention Window in getHealth Response

```

type HealthCheckResult struct {
	Status                string `json:"status"`
	LatestLedger          uint32 `json:"latestLedger"` // New
	OldestLedger          uint32 `json:"oldestLedger"` // New
	LedgerRetentionWindow uint32 `json:"ledgerRetentionWindow"` // New
}

```

### **Horizon API Changes:**

- There are no changes to Horizon APIs




## Reference Implementations

Reference implementation authored by SDF:

  - JavaScript: https://github.com/stellar/js-stellar-sdk/issues/935

You can follow each respective issue to its implementation PRs.



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b61a5e5..6ea5792a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@ Release History
 
 ### Pending
 #### Update
+- feat: add support for Soroban-RPC 21.
 - improve: The function in `stellar_xdr.scval` can accept `sc_val` of `base64` and `bytes` types.
 
 ### Version 9.4.0
diff --git a/stellar_sdk/soroban_rpc.py b/stellar_sdk/soroban_rpc.py
index 7c5e7f4c..67779f65 100644
--- a/stellar_sdk/soroban_rpc.py
+++ b/stellar_sdk/soroban_rpc.py
@@ -64,6 +64,7 @@ class EventInfo(BaseModel):
     topic: List[str] = Field(alias="topic")
     value: str = Field(alias="value")
     in_successful_contract_call: bool = Field(alias="inSuccessfulContractCall")
+    transaction_hash: str = Field(alias="txHash")
 
 
 class PaginationOptions(BaseModel):
@@ -142,6 +143,9 @@ class GetHealthResponse(BaseModel):
     """
 
     status: str
+    latest_ledger: int = Field(alias="latestLedger")
+    oldest_ledger: int = Field(alias="oldestLedger")
+    ledger_retention_window: int = Field(alias="ledgerRetentionWindow")
 
 
 # simulate_transaction
@@ -194,6 +198,21 @@ class RestorePreamble(BaseModel):
     min_resource_fee: int = Field(alias="minResourceFee")
 
 
+class LedgerEntryChange(BaseModel):
+    """LedgerEntryChange designates a change in a ledger entry. Before and After cannot be omitted at the same time.
+    If Before is omitted, it constitutes a creation, if After is omitted, it constitutes a deletion.
+    """
+
+    # LedgerEntryChangeType
+    type: str
+    # LedgerEntryKey in base64
+    key: str
+    # LedgerEntry XDR in base64
+    before: Optional[str] = None
+    # LedgerEntry XDR in base64
+    after: Optional[str] = None
+
+
 class SimulateTransactionResponse(BaseModel):
     """Response for JSON-RPC method simulateTransaction.
 
@@ -215,6 +234,10 @@ class SimulateTransactionResponse(BaseModel):
     restore_preamble: Optional[RestorePreamble] = Field(
         alias="restorePreamble", default=None
     )
+    # If present, it indicates how the state (ledger entries) will change as a result of the transaction execution.
+    state_changes: Optional[List[LedgerEntryChange]] = Field(
+        alias="stateChanges", default=None
+    )
     # If present, it indicates that a prior RestoreFootprint is required
     latest_ledger: int = Field(alias="latestLedger")
 


Test patch:
diff --git a/tests/test_soroban_server_async.py b/tests/test_soroban_server_async.py
index 25767b8c..dfbb3e6d 100644
--- a/tests/test_soroban_server_async.py
+++ b/tests/test_soroban_server_async.py
@@ -80,7 +80,12 @@ async def test_load_account_not_found_raise(self):
         }
 
     async def test_get_health(self):
-        result = {"status": "healthy"}
+        result = {
+            "status": "healthy",
+            "latestLedger": 50000,
+            "oldestLedger": 1,
+            "ledgerRetentionWindow": 10000,
+        }
         data = {
             "jsonrpc": "2.0",
             "id": "198cb1a8-9104-4446-a269-88bf000c2721",
@@ -293,6 +298,7 @@ async def test_get_events(self):
                     ],
                     "value": "AAAAAwAAAAE=",
                     "inSuccessfulContractCall": True,
+                    "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d",
                 },
                 {
                     "type": "contract",
@@ -307,6 +313,7 @@ async def test_get_events(self):
                     ],
                     "value": "AAAAAwAAAAI=",
                     "inSuccessfulContractCall": True,
+                    "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d",
                 },
             ],
             "latestLedger": "187",
@@ -400,6 +407,14 @@ async def test_simulate_transaction(self):
                 }
             ],
             "cost": {"cpuInsns": "1240100", "memBytes": "161637"},
+            "stateChanges": [
+                {
+                    "type": "created",
+                    "key": "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==",
+                    "before": None,
+                    "after": "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+                }
+            ],
             "latestLedger": "1479",
         }
         data = {
diff --git a/tests/test_soroban_server_sync.py b/tests/test_soroban_server_sync.py
index 6a80e0a6..21cf6fec 100644
--- a/tests/test_soroban_server_sync.py
+++ b/tests/test_soroban_server_sync.py
@@ -76,7 +76,12 @@ def test_load_account_not_found_raise(self):
         }
 
     def test_get_health(self):
-        result = {"status": "healthy"}
+        result = {
+            "status": "healthy",
+            "latestLedger": 50000,
+            "oldestLedger": 1,
+            "ledgerRetentionWindow": 10000,
+        }
         data = {
             "jsonrpc": "2.0",
             "id": "198cb1a8-9104-4446-a269-88bf000c2721",
@@ -285,6 +290,7 @@ def test_get_events(self):
                     ],
                     "value": "AAAAAwAAAAE=",
                     "inSuccessfulContractCall": True,
+                    "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d",
                 },
                 {
                     "type": "contract",
@@ -299,6 +305,7 @@ def test_get_events(self):
                     ],
                     "value": "AAAAAwAAAAI=",
                     "inSuccessfulContractCall": True,
+                    "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d",
                 },
             ],
             "latestLedger": "187",
@@ -390,6 +397,14 @@ def test_simulate_transaction(self):
                 }
             ],
             "cost": {"cpuInsns": "1240100", "memBytes": "161637"},
+            "stateChanges": [
+                {
+                    "type": "created",
+                    "key": "AAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWg==",
+                    "before": None,
+                    "after": "AAAAZAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+                }
+            ],
             "latestLedger": "1479",
         }
         data = {