diff --git a/examples/contract-deploy-update-destroy.py b/examples/contract-deploy-update-destroy.py
index b7402ae..d839eff 100644
--- a/examples/contract-deploy-update-destroy.py
+++ b/examples/contract-deploy-update-destroy.py
@@ -38,8 +38,8 @@ async def main(neoxp: shared.NeoExpress):
     contract = GenericContract(contract_hash)
     print("Calling `add` with input 1, result is: ", end="")
     # using test_invoke here because we don't really care about the result being persisted to the chain
-    result = await facade.test_invoke(contract.call_function("add", [1]))
-    print(unwrap.as_int(result))
+    receipt = await facade.test_invoke(contract.call_function("add", [1]))
+    print(unwrap.as_int(receipt.result))
 
     print("Updating contract with version 2...", end="")
     nef_v2 = nef.NEF.from_file(files_path + "contract_v2.nef")
@@ -52,8 +52,8 @@ async def main(neoxp: shared.NeoExpress):
 
     print("Calling `add` with input 1, result is: ", end="")
     # Using test_invoke here because we don't really care about the result being persisted to the chain
-    result = await facade.test_invoke(contract.call_function("add", [1]))
-    print(unwrap.as_int(result))
+    receipt = await facade.test_invoke(contract.call_function("add", [1]))
+    print(unwrap.as_int(receipt.result))
 
     print("Destroying contract...", end="")
     # destroy also doesn't give any return value. So if it doesn't fail then it means success
diff --git a/examples/nep-11-airdrop.py b/examples/nep-11-airdrop.py
index d82cabe..3e5d022 100644
--- a/examples/nep-11-airdrop.py
+++ b/examples/nep-11-airdrop.py
@@ -21,8 +21,8 @@ async def example_airdrop(neoxp: shared.NeoExpress):
 
     # Wrap the NFT contract
     ntf = NEP11NonDivisibleContract(shared.nep11_token_hash)
-    balance = len(await facade.test_invoke(ntf.token_ids_owned_by(account.address)))
-    print(f"Current NFT balance: {balance}")
+    receipt = await facade.test_invoke(ntf.token_ids_owned_by(account.address))
+    print(f"Current NFT balance: {len(receipt.result)}")
 
     # First we have to mint the NFTs to our own wallet
     # We do this by sending 10 GAS to the contract. We do this in 2 separate transactions because the NFT is
@@ -47,7 +47,8 @@ async def example_airdrop(neoxp: shared.NeoExpress):
         )
     )
     print(receipt.result)
-    token_ids = await facade.test_invoke(ntf.token_ids_owned_by(account.address))
+    receipt = await facade.test_invoke(ntf.token_ids_owned_by(account.address))
+    token_ids = receipt.result
     print(f"New NFT token balance: {len(token_ids)}, ids: {token_ids}")
 
     # Now let's airdrop the NFTs
diff --git a/examples/nep17-airdrop.py b/examples/nep17-airdrop.py
index 654a65b..5dbf65e 100644
--- a/examples/nep17-airdrop.py
+++ b/examples/nep17-airdrop.py
@@ -23,8 +23,8 @@ async def example_airdrop(neoxp: shared.NeoExpress):
 
     # Use the generic NEP17 class to wrap the token
     token = NEP17Contract(shared.coz_token_hash)
-    balance = await facade.test_invoke(token.balance_of(account.address))
-    print(f"Current COZ token balance: {balance}")
+    receipt = await facade.test_invoke(token.balance_of(account.address))
+    print(f"Current COZ token balance: {receipt.result}")
 
     # First we have to mint the tokens to our own wallet
     # We do this by sending NEO to the contract
@@ -46,8 +46,8 @@ async def example_airdrop(neoxp: shared.NeoExpress):
 
     print(receipt.result)
 
-    balance = await facade.test_invoke(token.balance_of(account.address))
-    print(f"New COZ token balance: {balance}")
+    receipt = await facade.test_invoke(token.balance_of(account.address))
+    print(f"New COZ token balance: {receipt.result}")
 
     # Now let's airdrop the tokens
     destination_addresses = [
diff --git a/examples/nep17-transfer.py b/examples/nep17-transfer.py
index c725894..86d850d 100644
--- a/examples/nep17-transfer.py
+++ b/examples/nep17-transfer.py
@@ -42,7 +42,7 @@ async def example_transfer_other(neoxp: shared.NeoExpress):
     facade = ChainFacade(rpc_host=neoxp.rpc_host)
     facade.add_signer(
         sign_insecure_with_account(account, password="123"),
-        Signer(account.script_hash),  # default scope is CALLED_BY_ENTRY
+        Signer(account.script_hash),  # default scope is te/CALLED_BY_ENTRY
     )
 
     source = account.address
diff --git a/examples/vote.py b/examples/vote.py
index de44e25..064946a 100644
--- a/examples/vote.py
+++ b/examples/vote.py
@@ -22,7 +22,8 @@ async def example_vote(neoxp: shared.NeoExpress):
     # Dedicated Neo native contract wrapper
     neo = NeoToken()
     # get a list of candidates that can be voted on
-    candidates = await facade.test_invoke(neo.candidates_registered())
+    receipt = await facade.test_invoke(neo.candidates_registered())
+    candidates = receipt.result
     # the example chain only has 1 candidate, use that
     candidate_pk = candidates[0].public_key
 
diff --git a/neo3/api/wrappers.py b/neo3/api/wrappers.py
index 2dd7b98..88e0d5a 100644
--- a/neo3/api/wrappers.py
+++ b/neo3/api/wrappers.py
@@ -159,7 +159,7 @@ async def test_invoke(
         f: ContractMethodResult[ReturnType],
         *,
         signers: Optional[Sequence[verification.Signer]] = None,
-    ) -> ReturnType:
+    ) -> InvokeReceipt[ReturnType]:
         """
         Call a contract method in read-only mode.
         This does not persist any state on the actual chain and therefore does not require signing or paying GAS.
@@ -180,9 +180,9 @@ async def test_invoke_multi(
         f: list[ContractMethodResult],
         *,
         signers: Optional[Sequence[verification.Signer]] = None,
-    ) -> Sequence:
+    ) -> InvokeReceipt[Sequence]:
         """
-        Call all contract methods in one go (concurrently) and return the list of results.
+        Call all contract methods in one go (concatenated in 1 script) and return the list of results.
 
         Args:
             f: list of functions to call.
@@ -193,8 +193,35 @@ async def test_invoke_multi(
         """
         if signers is None:
             signers = self.signers
-        return await asyncio.gather(
-            *map(lambda c: self.test_invoke(c, signers=signers), f)
+        script = bytearray()
+        for call in f:  # type: ContractMethodResult
+            script.extend(call.script)
+
+        wrapped: ContractMethodResult[None] = ContractMethodResult(script)
+        receipt = await self.test_invoke_raw(wrapped, signers=signers)
+
+        results = []
+        stack_offset = 0
+        for call in f:
+            res_cpy = deepcopy(receipt.result)
+            # adjust the stack so that it becomes transparent for the post-processing functions.
+            res_cpy.stack = res_cpy.stack[
+                stack_offset : stack_offset + call.return_count
+            ]
+            if call.execution_processor is None:
+                results.append(res_cpy)
+            else:
+                results.append(call.execution_processor(res_cpy, 0))
+            stack_offset += call.return_count
+        return InvokeReceipt[Sequence](
+            receipt.tx_hash,
+            receipt.included_in_block,
+            receipt.confirmations,
+            receipt.gas_consumed,
+            receipt.state,
+            receipt.exception,
+            receipt.notifications,
+            results,
         )
 
     async def test_invoke_raw(
@@ -202,7 +229,7 @@ async def test_invoke_raw(
         f: ContractMethodResult[ReturnType],
         *,
         signers: Optional[Sequence[verification.Signer]] = None,
-    ) -> noderpc.ExecutionResult:
+    ) -> InvokeReceipt[noderpc.ExecutionResult]:
         """
         Call a contract method in read-only mode.
         This does not persist any state on the actual chain and therefore does not require signing or paying GAS.
@@ -217,7 +244,8 @@ async def test_invoke_raw(
         """
         if signers is None:
             signers = self.signers
-        return await self._test_invoke(f, signers=signers, return_raw=True)
+        res = await self._test_invoke(f, signers=signers, return_raw=True)
+        return cast(InvokeReceipt[noderpc.ExecutionResult], res)
 
     async def _test_invoke(
         self,
@@ -225,7 +253,7 @@ async def _test_invoke(
         *,
         signers: Optional[Sequence[verification.Signer]] = None,
         return_raw: Optional[bool] = False,
-    ):
+    ) -> InvokeReceipt[ReturnType]:
         """
         Args:
             f:
@@ -234,9 +262,21 @@ async def _test_invoke(
         """
         async with noderpc.NeoRpcClient(self.rpc_host) as client:
             res = await client.invoke_script(f.script, signers)
+
             if f.execution_processor is None or return_raw or res.state != "HALT":
-                return res
-            return f.execution_processor(res, 0)
+                result = res
+            else:
+                result = f.execution_processor(res, 0)
+            return InvokeReceipt[ReturnType](
+                types.UInt256.zero(),
+                -1,
+                -1,
+                res.gas_consumed,
+                res.state,
+                res.exception,
+                res.notifications,
+                result,
+            )
 
     async def invoke(
         self,
@@ -448,7 +488,7 @@ async def invoke_multi(
         append_network_fee: int = 0,
         append_system_fee: int = 0,
         _post_processing: bool = True,
-    ) -> Sequence:
+    ) -> InvokeReceipt[Sequence]:
         """
         Call all contract methods (concatenated) in one go and persist results on the chain. Costs GAS.
         Waits for tx to be included in a block. Automatically post processes the execution results according to the
@@ -499,7 +539,16 @@ async def invoke_multi(
             else:
                 results.append(call.execution_processor(res_cpy, 0))
             stack_offset += call.return_count
-        return results
+        return InvokeReceipt[Sequence](
+            receipt.tx_hash,
+            receipt.included_in_block,
+            receipt.confirmations,
+            receipt.execution.gas_consumed,
+            receipt.execution.state,
+            receipt.execution.exception,
+            receipt.execution.notifications,
+            results,
+        )
 
     async def invoke_multi_fast(
         self,
@@ -560,7 +609,7 @@ async def invoke_multi_raw(
         system_fee: int = 0,
         append_network_fee: int = 0,
         append_system_fee: int = 0,
-    ) -> Sequence:
+    ) -> InvokeReceipt[Sequence]:
         """
         Call all contract methods (concatenated) in one go and persist results on the chain. Costs GAS.
         Do not wait for tx to be included in a block. Do not post process the execution results according to
@@ -1288,7 +1337,6 @@ def process(res: noderpc.ExecutionResult, _: int = 0) -> list[types.UInt160]:
 
         return ContractMethodResult(sb.to_array(), process)
 
-
     def total_owned_by(
         self, owner: types.UInt160 | NeoAddress, token_id: bytes
     ) -> ContractMethodResult[int]:
@@ -1304,7 +1352,6 @@ def total_owned_by(
         )
         return ContractMethodResult(sb.to_array(), unwrap.as_int)
 
-
     def total_owned_by_friendly(
         self, owner: types.UInt160 | NeoAddress, token_id: bytes
     ) -> ContractMethodResult[float]: