> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sign EVM Transactions

> Send and sign Ethereum transactions using Dynamic's Python SDK

## Overview

The Python SDK exposes two methods for transactions on EVM chains:

* **`send_transaction`** — signs a legacy transaction and broadcasts it via JSON-RPC in a single call. Returns the transaction hash.
* **`sign_transaction`** — signs a legacy transaction and returns the raw ECDSA signature (`0x` + r + s + v, 132 chars). Broadcasting is your responsibility.

`send_transaction` is the recommended path for most apps. Reach for `sign_transaction` only if you need the signature out-of-band — co-signing, custom relayers, or assembling the raw tx yourself.

## Prerequisites

* [Created an EVM wallet](/python/evm/create-wallet)

## Quick Start: Send a Transaction

`send_transaction` signs and broadcasts in one call. Pass a JSON-RPC endpoint either per-call via `rpc_url=`, or once at construction via `rpc_urls={chainId: url}`.

```python theme={"system"}
import asyncio
import os
from dynamic_wallet_sdk import DynamicEvmWalletClient

async def main():
    async with DynamicEvmWalletClient(
        os.environ["DYNAMIC_ENV_ID"],
        rpc_urls={1: "https://mainnet.infura.io/v3/YOUR_PROJECT_ID"},
    ) as client:
        await client.authenticate_api_token(os.environ["DYNAMIC_API_TOKEN"])

        tx = {
            "to": "0xRecipientAddress",
            "value": 100_000_000_000_000_000,  # 0.1 ETH
            "nonce": 0,
            "gas": 21000,
            "gasPrice": 20_000_000_000,
            "chainId": 1,
            "data": "0x",
        }

        tx_hash = await client.send_transaction(
            address="0xYourWalletAddress",
            tx=tx,
        )
        print(f"Transaction hash: {tx_hash}")
        print(f"Explorer: https://etherscan.io/tx/{tx_hash}")

asyncio.run(main())
```

You can also pass `rpc_url=` per call, which overrides the constructor mapping:

```python theme={"system"}
tx_hash = await client.send_transaction(
    address="0xYourWalletAddress",
    tx=tx,
    rpc_url="https://mainnet.infura.io/v3/YOUR_PROJECT_ID",
)
```

The SDK adds **EIP-155 replay protection** automatically based on `chainId`.

## Signing with a Password-Protected Wallet

If the wallet was created with a password, pass it via `password=`. The same kwarg also auto-recovers key shares in stateless or fresh processes — see [Step 3 of Create an EVM Wallet](/python/evm/create-wallet).

```python theme={"system"}
tx_hash = await client.send_transaction(
    address="0xYourWalletAddress",
    tx=tx,
    password="your-wallet-password",
)
```

## Common Transaction Types

### ETH Transfer

```python theme={"system"}
tx = {
    "to": "0xRecipientAddress",
    "value": 100_000_000_000_000_000,  # 0.1 ETH in wei
    "nonce": nonce,
    "gas": 21000,
    "gasPrice": gas_price,
    "chainId": 1,
    "data": "0x",
}
```

### Contract Interaction

```python theme={"system"}
tx = {
    "to": "0xContractAddress",
    "value": 0,
    "nonce": nonce,
    "gas": 100000,
    "gasPrice": gas_price,
    "chainId": 1,
    "data": "0xFunctionSelector...",  # ABI-encoded call data
}
```

## EIP-1559 Support

EIP-1559 (Type 2) transactions are not yet supported — the SDK currently signs legacy transactions only. Use `gasPrice` rather than `maxFeePerGas` / `maxPriorityFeePerGas`. Passing EIP-1559 fields raises `DynamicSDKError`.

## Manual Signing and Broadcasting

If you need the raw signature — for offline workflows, custom broadcast logic, or co-signing — call `sign_transaction` directly. It returns just the **65-byte ECDSA signature** as a `0x`-prefixed hex string (`r` + `s` + `v`, 132 chars). It does **not** return an RLP-encoded transaction.

```python theme={"system"}
import asyncio
import os
from dynamic_wallet_sdk import DynamicEvmWalletClient

async def main():
    async with DynamicEvmWalletClient(os.environ["DYNAMIC_ENV_ID"]) as client:
        await client.authenticate_api_token(os.environ["DYNAMIC_API_TOKEN"])

        tx = {
            "to": "0xRecipientAddress",
            "value": 0,
            "nonce": 0,
            "gas": 21000,
            "gasPrice": 20_000_000_000,
            "chainId": 1,
            "data": "0x",
        }

        sig_hex = await client.sign_transaction(
            address="0xYourWalletAddress",
            tx=tx,
        )
        print(f"Signature: {sig_hex}")

asyncio.run(main())
```

To broadcast yourself, combine the signature with the unsigned tx and apply EIP-155 `v` adjustment:

```python theme={"system"}
import rlp
from eth_account._utils.legacy_transactions import (
    encode_transaction,
    serializable_unsigned_transaction_from_dict,
)
from web3 import Web3

r = int(sig_hex[2:66], 16)
s = int(sig_hex[66:130], 16)
v = int(sig_hex[130:132], 16)

# EIP-155: v + chainId * 2 + 8
v = v + tx["chainId"] * 2 + 8

unsigned = serializable_unsigned_transaction_from_dict(tx)
raw_signed = encode_transaction(unsigned, vrs=(v, r, s))

w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID"))
tx_hash = w3.eth.send_raw_transaction(raw_signed)
print(f"Transaction hash: {tx_hash.hex()}")
```

## Error Handling

```python theme={"system"}
from dynamic_wallet_sdk import (
    DynamicSDKError,
    WalletNotFoundError,
)

try:
    tx_hash = await client.send_transaction(address=address, tx=tx)
except WalletNotFoundError:
    # Address not loaded into the client — rehydrate first.
    await client.load_wallet(address)
    tx_hash = await client.send_transaction(
        address=address,
        tx=tx,
        password=password,
    )
except DynamicSDKError as e:
    print(f"Send failed: {e}")
```

## Next Steps

* [Sign messages](/python/evm/sign-messages)
* [Sign typed data (EIP-712)](/python/evm/sign-typed-data)
* [Delegated access](/python/evm/delegated-access)
