Skip to content
5 changes: 5 additions & 0 deletions .changelog/brave-cats-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pympp: minor
---

Added split payments support for Tempo charges, allowing a single charge to be split across multiple recipients. Port of [mpp-rs PR #180](https://github.com/tempoxyz/mpp-rs/pull/180).
5 changes: 5 additions & 0 deletions .changelog/shy-frogs-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pympp: minor
---

Added split payments support for Tempo charges, allowing a single charge to be split across multiple recipients. Port of [mpp-rs PR #180](https://github.com/tempoxyz/mpp-rs/pull/180).
3 changes: 2 additions & 1 deletion src/mpp/methods/tempo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
_LAZY_EXPORTS = {
"mpp.methods.tempo.account": ("TempoAccount",),
"mpp.methods.tempo.client": ("TempoMethod", "TransactionError", "tempo"),
"mpp.methods.tempo.intents": ("ChargeIntent",),
"mpp.methods.tempo.intents": ("ChargeIntent", "Transfer", "get_transfers"),
"mpp.methods.tempo.schemas": ("Split",),
}


Expand Down
6 changes: 6 additions & 0 deletions src/mpp/methods/tempo/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ from mpp.methods.tempo.client import TempoMethod as _TempoMethod
from mpp.methods.tempo.client import TransactionError as _TransactionError
from mpp.methods.tempo.client import tempo as _tempo
from mpp.methods.tempo.intents import ChargeIntent as _ChargeIntent
from mpp.methods.tempo.intents import Transfer as _Transfer
from mpp.methods.tempo.intents import get_transfers as _get_transfers
from mpp.methods.tempo.schemas import Split as _Split

CHAIN_ID = _CHAIN_ID
ESCROW_CONTRACTS = _ESCROW_CONTRACTS
Expand All @@ -23,3 +26,6 @@ TempoMethod = _TempoMethod
TransactionError = _TransactionError
tempo = _tempo
ChargeIntent = _ChargeIntent
Transfer = _Transfer
get_transfers = _get_transfers
Split = _Split
45 changes: 39 additions & 6 deletions src/mpp/methods/tempo/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ async def create_credential(self, challenge: Challenge) -> Credential:
client_id=self.client_id,
)

splits = method_details.get("splits") if isinstance(method_details, dict) else None

# Resolve RPC URL from challenge's chainId (like mppx), falling back
# to the method-level rpc_url.
rpc_url = self.rpc_url
Expand Down Expand Up @@ -159,6 +161,7 @@ async def create_credential(self, challenge: Challenge) -> Credential:
rpc_url=rpc_url,
expected_chain_id=expected_chain_id,
awaiting_fee_payer=use_fee_payer,
splits=splits,
)

# When signing with an access key, the credential source is the
Expand All @@ -181,6 +184,7 @@ async def _build_tempo_transfer(
rpc_url: str | None = None,
expected_chain_id: int | None = None,
awaiting_fee_payer: bool = False,
splits: list[dict] | None = None,
) -> tuple[str, int]:
"""Build a client-signed Tempo transaction.

Expand Down Expand Up @@ -215,10 +219,29 @@ async def _build_tempo_transfer(

resolved_rpc = rpc_url or self.rpc_url

if memo:
transfer_data = self._encode_transfer_with_memo(recipient, int(amount), memo)
gas_estimate_data: str | None = None

if splits:
from mpp.methods.tempo.intents import get_transfers
from mpp.methods.tempo.schemas import Split as SplitModel

parsed_splits = [SplitModel(**s) for s in splits]
transfer_list = get_transfers(int(amount), recipient, memo, parsed_splits)
call_list = []
for t in transfer_list:
if t.memo is not None:
td = self._encode_transfer_with_memo(t.recipient, t.amount, "0x" + t.memo.hex())
else:
td = self._encode_transfer(t.recipient, t.amount)
call_list.append(Call.create(to=currency, value=0, data=td))
calls_tuple = tuple(call_list)
else:
transfer_data = self._encode_transfer(recipient, int(amount))
if memo:
transfer_data = self._encode_transfer_with_memo(recipient, int(amount), memo)
else:
transfer_data = self._encode_transfer(recipient, int(amount))
calls_tuple = (Call.create(to=currency, value=0, data=transfer_data),)
gas_estimate_data = transfer_data

# When using an access key, fetch nonce from the root account
# (smart wallet), not the access key address.
Expand All @@ -243,8 +266,18 @@ async def _build_tempo_transfer(

gas_limit = DEFAULT_GAS_LIMIT
try:
estimated = await estimate_gas(resolved_rpc, nonce_address, currency, transfer_data)
gas_limit = max(gas_limit, estimated + 5_000)
if splits:
total_estimated = 0
for c in calls_tuple:
total_estimated += await estimate_gas(
resolved_rpc, nonce_address, currency, c.data.hex()
)
gas_limit = max(gas_limit, total_estimated + 5_000 * len(calls_tuple))
elif gas_estimate_data is not None:
estimated = await estimate_gas(
resolved_rpc, nonce_address, currency, gas_estimate_data
)
gas_limit = max(gas_limit, estimated + 5_000)
except Exception:
pass

Expand All @@ -258,7 +291,7 @@ async def _build_tempo_transfer(
fee_token=None if awaiting_fee_payer else currency,
awaiting_fee_payer=awaiting_fee_payer,
valid_before=valid_before,
calls=(Call.create(to=currency, value=0, data=transfer_data),),
calls=calls_tuple,
)

if self.root_account:
Expand Down
Loading
Loading