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

# Manage Token Delegations

> Learn how to approve, revoke, and manage token delegations using Portal's iOS SDK.

Portal's iOS SDK provides token delegation capabilities through the `portal.delegations` API. This enables approving token spending, revoking approvals, checking delegation status, and transferring tokens as a delegate on both EVM and Solana chains.

## Overview

The delegations functionality allows you to:

* **Approve** other addresses to spend tokens on behalf of your wallet
* **Revoke** existing delegations to remove spending permissions
* **Check status** of active delegations and balances
* **Transfer tokens** as a delegate from another address

## Prerequisites

Before using delegation operations, ensure you have:

* A properly initialized Portal client
* An active wallet with tokens on the target network (see [Create a wallet](./create-a-wallet))
* Understanding of [token delegations concepts](/resources/delegations)

<Warning>
  Delegations apply to ERC-20 tokens (EVM) and SPL Tokens (Solana) only. Native assets like ETH, MON, and SOL cannot be delegated — they have no on-chain `approve` / `transferFrom` (or SPL delegate) semantics. Calls using a native asset identifier will be rejected. See [Delegations](/resources/delegations#what-are-token-delegations) for the protocol-level reason and workarounds.
</Warning>

## High-Level Methods

Use `approveAndSubmit`, `revokeAndSubmit`, and `transferAndSubmit` when you want one call for the whole flow: build the delegation transaction(s), then sign and broadcast each one in order and collect the resulting hashes. `Portal` installs a working signer on `portal.delegations` for you, so the common case needs no configuration at all.

<Warning>
  `approveAndSubmit`, `revokeAndSubmit`, and `transferAndSubmit` broadcast each transaction and return as soon as it is accepted by the network. They **do not wait for on-chain confirmation**. A hash in `DelegationSubmitResult.hashes` means the transaction was submitted, not that it succeeded — an approval can still revert. If your flow depends on the delegation being active, wait for the receipt yourself, or poll `getStatus` before proceeding.

  This is different from [Yield.xyz](./yield-xyz), where you wait for each transaction to confirm before moving on to the next step. Do not carry that assumption over to delegations.
</Warning>

### Signatures

```swift theme={null}
func setSignAndSendTransaction(_ fn: @escaping DelegationSignAndSend)

func approveAndSubmit(
    request: ApproveDelegationRequest,
    options: DelegationSubmitOptions
) async throws -> DelegationSubmitResult

func revokeAndSubmit(
    request: RevokeDelegationRequest,
    options: DelegationSubmitOptions
) async throws -> DelegationSubmitResult

func transferAndSubmit(
    request: TransferFromRequest,
    options: DelegationSubmitOptions
) async throws -> DelegationSubmitResult
```

Each submit method also has a no-options convenience overload, which is what most callers want since `Portal` has already installed a signer:

```swift theme={null}
func approveAndSubmit(request: ApproveDelegationRequest) async throws -> DelegationSubmitResult
func revokeAndSubmit(request: RevokeDelegationRequest) async throws -> DelegationSubmitResult
func transferAndSubmit(request: TransferFromRequest) async throws -> DelegationSubmitResult
```

The request types are the same ones the low-level methods take — see [EVM Approval](#evm-approval), [EVM Revoke](#evm-revoke), and [EVM Transfer From](#evm-transfer-from) below for their fields.

### Configuring the signer

A signer signs and broadcasts one transaction and returns its hash:

```swift theme={null}
public typealias DelegationSignAndSend = (
    _ transaction: DelegationTransaction,
    _ chainId: String
) async throws -> String
```

`DelegationTransaction` covers both ecosystems:

```swift theme={null}
public enum DelegationTransaction: Equatable {
    case evm(ConstructedEipTransaction)
    case solana(String) // base64-encoded

    public var evmTransaction: ConstructedEipTransaction? { get }
    public var solanaTransaction: String? { get }
}
```

`Portal` installs a default signer that routes `.evm` transactions to `eth_sendTransaction` and `.solana` transactions to `sol_signAndSendTransaction`, so this works with zero setup:

```swift theme={null}
let result = try await portal.delegations.approveAndSubmit(request: request)
```

Use `setSignAndSendTransaction(_:)` to replace the signer for the instance, or `DelegationSubmitOptions.signAndSendTransaction` to override it for a single call. The precedence is **per-call option → instance signer → `Portal` default**.

### Options and progress

`DelegationSubmitOptions` is the second argument to each submit method:

| Parameter                | Type                                    | Required | Description                                                                                                 |
| ------------------------ | --------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `signAndSendTransaction` | `DelegationSignAndSend?`                | No       | Per-call signer override. Takes priority over the instance signer set with `setSignAndSendTransaction(_:)`. |
| `onProgress`             | `((DelegationSubmitProgress) -> Void)?` | No       | Called as each transaction is signed and submitted.                                                         |

`DelegationSubmitProgress` carries:

| Field   | Type                   | Description                                                              |
| ------- | ---------------------- | ------------------------------------------------------------------------ |
| `step`  | `DelegationSubmitStep` | `.signing` or `.submitted`.                                              |
| `index` | `Int`                  | The 0-based index of this transaction in the sequence.                   |
| `total` | `Int`                  | The total number of transactions in the sequence.                        |
| `hash`  | `String?`              | `nil` on `.signing`, and the broadcast transaction hash on `.submitted`. |

`DelegationSubmitStep` has exactly two cases — `signing` and `submitted`. There is no confirming or confirmed step, because nothing is awaited on-chain.

### Return value

`DelegationSubmitResult` carries only the hashes:

| Field    | Type       | Description                                              |
| -------- | ---------- | -------------------------------------------------------- |
| `hashes` | `[String]` | One hash per broadcast transaction, in submission order. |

There is no status field, and no partial-success concept: a hash is present because the network accepted the transaction, and nothing beyond that has been checked.

### Example (approve and submit)

This builds the same approval as [EVM Approval](#evm-approval) below, but signs and broadcasts it for you.

```swift theme={null}
import PortalSwift

Task {
    do {
        let request = ApproveDelegationRequest(
            chain: "eip155:11155111",
            token: "USDC",
            delegateAddress: "0xa944e86eb36f039becd1843132347eb5b8501562",
            amount: "0.01"
        )

        let result = try await portal.delegations.approveAndSubmit(
            request: request,
            options: DelegationSubmitOptions(
                onProgress: { progress in
                    switch progress.step {
                    case .signing:
                        print("Signing \(progress.index + 1)/\(progress.total)")
                    case .submitted:
                        print("Submitted: \(progress.hash ?? "")")
                    }
                }
            )
        )

        // These are broadcast, not confirmed.
        print("Submitted hashes: \(result.hashes)")
    } catch DelegationsError.noTransactions {
        print("The approval response contained no transactions to submit.")
    } catch DelegationsError.invalidTransactionHash(let index, let chainId) {
        print("Signer returned an unusable hash for transaction \(index) on \(chainId)")
    } catch {
        print("Error approving and submitting delegation: \(error)")
    }
}
```

The `switch` over `progress.step` is exhaustive with no `default:` — there really are only two steps.

### Example (revoke and submit)

Same `RevokeDelegationRequest` as [EVM Revoke](#evm-revoke) below, submitted end to end.

```swift theme={null}
import PortalSwift

Task {
    do {
        let request = RevokeDelegationRequest(
            chain: "eip155:11155111",
            token: "USDC",
            delegateAddress: "0xa944e86eb36f039becd1843132347eb5b8501562"
        )

        let result = try await portal.delegations.revokeAndSubmit(request: request)

        print("Revoke submitted: \(result.hashes)")
    } catch DelegationsError.noTransactions {
        print("The revoke response contained no transactions to submit.")
    } catch {
        print("Error revoking delegation: \(error)")
    }
}
```

<Warning>
  Because the revoke is not confirmed when this call returns, the delegation may still be active for a short time afterwards. Poll `getStatus` if you need to show the user that it is gone.
</Warning>

### Example (transfer as a delegate)

Same `TransferFromRequest` as [EVM Transfer From](#evm-transfer-from) below. Your wallet must already be an approved delegate for `fromAddress`.

```swift theme={null}
import PortalSwift

Task {
    do {
        let request = TransferFromRequest(
            chain: "eip155:11155111",
            token: "USDC",
            fromAddress: "0x099699ed181517d4ce0ba4487bea671d31bb1db5", // Token owner
            toAddress: "0xdFd8302f44727A6348F702fF7B594f127dE3A902", // Recipient
            amount: "0.01"
        )

        let result = try await portal.delegations.transferAndSubmit(request: request)

        print("Transfer submitted: \(result.hashes)")
    } catch DelegationsError.noTransactions {
        print("The transfer response contained no transactions to submit.")
    } catch {
        print("Error transferring delegated tokens: \(error)")
    }
}
```

### Example (custom signer)

Replace the default signer when you need to do something it does not, such as attaching a `signatureApprovalMemo` to every delegation transaction. `evmTransaction` and `solanaTransaction` each return `nil` for the other case, so you can branch without a full `switch`:

```swift theme={null}
import PortalSwift

enum MyAppError: Error {
    case missingTransactionHash
    case unsupportedTransaction
}

func sendEvmDelegationTransaction(
    _ transaction: ConstructedEipTransaction,
    chainId: String
) async throws -> String {
    // `ConstructedEipTransaction` is `Codable`, so you can also encode it here for logging
    // or to hand it off to another process before signing.
    var params: [String: String] = [
        "from": transaction.from,
        "to": transaction.to
    ]
    if let data = transaction.data { params["data"] = data }
    if let value = transaction.value { params["value"] = value }

    let response = try await portal.request(
        chainId: chainId,
        method: .eth_sendTransaction,
        params: [params],
        options: RequestOptions(signatureApprovalMemo: "Delegation transaction")
    )
    guard let hash = response.result as? String else {
        throw MyAppError.missingTransactionHash
    }
    return hash
}

func sendSolanaDelegationTransaction(
    _ encodedTransaction: String,
    chainId: String
) async throws -> String {
    let response = try await portal.request(
        chainId: chainId,
        method: .sol_signAndSendTransaction,
        params: [encodedTransaction],
        options: RequestOptions(signatureApprovalMemo: "Delegation transaction")
    )
    guard let hash = response.result as? String else {
        throw MyAppError.missingTransactionHash
    }
    return hash
}

// Branch with the accessors
portal.delegations.setSignAndSendTransaction { transaction, chainId in
    if let evmTransaction = transaction.evmTransaction {
        return try await sendEvmDelegationTransaction(evmTransaction, chainId: chainId)
    }
    if let encodedTransaction = transaction.solanaTransaction {
        return try await sendSolanaDelegationTransaction(encodedTransaction, chainId: chainId)
    }
    throw MyAppError.unsupportedTransaction
}

// Or match the enum, when you want the compiler to make you handle both cases
portal.delegations.setSignAndSendTransaction { transaction, chainId in
    switch transaction {
    case let .evm(evmTransaction):
        return try await sendEvmDelegationTransaction(evmTransaction, chainId: chainId)
    case let .solana(encodedTransaction):
        return try await sendSolanaDelegationTransaction(encodedTransaction, chainId: chainId)
    }
}
```

To use a different signer for a single call, pass it in the options instead — it takes priority over the instance signer:

```swift theme={null}
let result = try await portal.delegations.approveAndSubmit(
    request: request,
    options: DelegationSubmitOptions(
        signAndSendTransaction: { transaction, chainId in
            guard let evmTransaction = transaction.evmTransaction else {
                throw MyAppError.unsupportedTransaction
            }
            return try await sendEvmDelegationTransaction(evmTransaction, chainId: chainId)
        }
    )
)
```

### Errors

The submit methods throw `DelegationsError` in addition to the network and decoding errors the low-level methods can throw.

| Case                                     | When                                                                                                                                                |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `noSignerConfigured`                     | No signer was available. This cannot happen on `portal.delegations`, which `Portal` wires for you — only when you construct `Delegations` yourself. |
| `noTransactions`                         | The delegation response contained no transactions to submit.                                                                                        |
| `invalidTransactionHash(index:chainId:)` | The signer returned a value that is not a usable hash for that chain. `index` identifies which transaction in the sequence it was.                  |

<Note>
  `DelegationsProtocol` gained `setSignAndSendTransaction`, `approveAndSubmit`, `revokeAndSubmit`, and `transferAndSubmit` in 7.3.0. Calling code is unaffected, but anything that *implements* the protocol — a hand-rolled test mock, for example — needs all four before it will compile.
</Note>

***

## Low-level methods

The sections below are the manual path: `approve`, `revoke`, `transferFrom`, and `getStatus` return unsigned transactions and leave signing and broadcasting to you. Use them when you need to inspect, modify, batch, or route the transactions yourself. Otherwise prefer the high-level methods above.

## Approving Delegations

Use `approve` to grant another address permission to spend tokens on your behalf. This method works for both EVM and Solana chains.

### EVM Approval

```swift theme={null}
Task {
    do {
        let request = ApproveDelegationRequest(
            chain: "eip155:11155111",
            token: "USDC",
            delegateAddress: "0xa944e86eb36f039becd1843132347eb5b8501562",
            amount: "0.01"
        )

        let response = try await portal.delegations.approve(request: request)

        // Sign and send EVM transactions sequentially
        if let transactions = response.transactions {
            for (index, tx) in transactions.enumerated() {
                var txDict: [String: String] = [
                    "from": tx.from,
                    "to": tx.to
                ]
                if let data = tx.data { txDict["data"] = data }
                if let value = tx.value { txDict["value"] = value }

                let txResponse = try await portal.request(
                    chainId: "eip155:11155111",
                    method: .eth_sendTransaction,
                    params: [txDict],
                    options: RequestOptions(signatureApprovalMemo: "Approve delegation")
                )
                print("Tx \(index + 1) hash: \(txResponse.result as? String ?? "Unknown")")
            }
        }
    } catch {
        print("Error approving EVM delegation: \(error)")
    }
}
```

### Solana Approval

```swift theme={null}
Task {
    do {
        let request = ApproveDelegationRequest(
            chain: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
            token: "USDC",
            delegateAddress: "7smgSuU5mjP7QY5yWGdaTfgKn8hUWwvQgfvgcZB3HmJi",
            amount: "0.01"
        )

        let response = try await portal.delegations.approve(request: request)

        // Sign and send Solana transactions sequentially
        if let encodedTransactions = response.encodedTransactions {
            for (index, encodedTx) in encodedTransactions.enumerated() {
                let txResponse = try await portal.request(
                    chainId: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
                    method: .sol_signAndSendTransaction,
                    params: [encodedTx],
                    options: RequestOptions(signatureApprovalMemo: "Approve delegation")
                )
                print("Tx \(index + 1) hash: \(txResponse.result as? String ?? "Unknown")")
            }
        }
    } catch {
        print("Error approving Solana delegation: \(error)")
    }
}
```

***

## Checking Delegation Status

Use `getStatus` to check current delegations and token balances for a specific delegate address.

### EVM Status Check

```swift theme={null}
Task {
    do {
        let request = GetDelegationStatusRequest(
            chain: "eip155:11155111",
            token: "USDC",
            delegateAddress: "0xa944e86eb36f039becd1843132347eb5b8501562"
        )

        let response = try await portal.delegations.getStatus(request: request)

        print("Chain ID: \(response.chainId)")
        print("Token: \(response.token)")
        print("Token Address: \(response.tokenAddress)")
        if let balance = response.balance { print("Balance: \(balance)") }
        print("Delegations: \(response.delegations.count)")

        for delegation in response.delegations {
            print("  - Address: \(delegation.address), Amount: \(delegation.delegateAmount)")
        }
    } catch {
        print("Error getting EVM delegation status: \(error)")
    }
}
```

### Solana Status Check

```swift theme={null}
Task {
    do {
        let request = GetDelegationStatusRequest(
            chain: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
            token: "USDC",
            delegateAddress: "7smgSuU5mjP7QY5yWGdaTfgKn8hUWwvQgfvgcZB3HmJi"
        )

        let response = try await portal.delegations.getStatus(request: request)

        print("Chain ID: \(response.chainId)")
        print("Token: \(response.token)")
        print("Delegations: \(response.delegations.count)")

        for delegation in response.delegations {
            print("  - Address: \(delegation.address), Amount: \(delegation.delegateAmount)")
        }
    } catch {
        print("Error getting Solana delegation status: \(error)")
    }
}
```

***

## Revoking Delegations

Use `revoke` to remove spending permissions from a delegate address.

### EVM Revoke

```swift theme={null}
Task {
    do {
        let request = RevokeDelegationRequest(
            chain: "eip155:11155111",
            token: "USDC",
            delegateAddress: "0xa944e86eb36f039becd1843132347eb5b8501562"
        )

        let response = try await portal.delegations.revoke(request: request)

        // Sign and send EVM transactions sequentially
        if let transactions = response.transactions {
            for (index, tx) in transactions.enumerated() {
                var txDict: [String: String] = [
                    "from": tx.from,
                    "to": tx.to
                ]
                if let data = tx.data { txDict["data"] = data }
                if let value = tx.value { txDict["value"] = value }

                let txResponse = try await portal.request(
                    chainId: "eip155:11155111",
                    method: .eth_sendTransaction,
                    params: [txDict],
                    options: RequestOptions(signatureApprovalMemo: "Revoke delegation")
                )
                print("Tx \(index + 1) hash: \(txResponse.result as? String ?? "Unknown")")
            }
        }
    } catch {
        print("Error revoking EVM delegation: \(error)")
    }
}
```

### Solana Revoke

```swift theme={null}
Task {
    do {
        let request = RevokeDelegationRequest(
            chain: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
            token: "USDC",
            delegateAddress: "7smgSuU5mjP7QY5yWGdaTfgKn8hUWwvQgfvgcZB3HmJi"
        )

        let response = try await portal.delegations.revoke(request: request)

        // Sign and send Solana transactions sequentially
        if let encodedTransactions = response.encodedTransactions {
            for (index, encodedTx) in encodedTransactions.enumerated() {
                let txResponse = try await portal.request(
                    chainId: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
                    method: .sol_signAndSendTransaction,
                    params: [encodedTx],
                    options: RequestOptions(signatureApprovalMemo: "Revoke delegation")
                )
                print("Tx \(index + 1) hash: \(txResponse.result as? String ?? "Unknown")")
            }
        }
    } catch {
        print("Error revoking Solana delegation: \(error)")
    }
}
```

<Warning>
  Always revoke unused delegations after completing operations to minimize security risks.
</Warning>

***

## Transferring as a Delegate

Use `transferFrom` to transfer tokens from another address that has delegated spending permission to you.

### EVM Transfer From

```swift theme={null}
Task {
    do {
        let request = TransferFromRequest(
            chain: "eip155:11155111",
            token: "USDC",
            fromAddress: "0x099699ed181517d4ce0ba4487bea671d31bb1db5", // Token owner
            toAddress: "0xdFd8302f44727A6348F702fF7B594f127dE3A902",   // Recipient
            amount: "0.01"
        )

        let response = try await portal.delegations.transferFrom(request: request)

        // Sign and send EVM transactions sequentially
        if let transactions = response.transactions {
            for (index, tx) in transactions.enumerated() {
                var txDict: [String: String] = [
                    "from": tx.from,
                    "to": tx.to
                ]
                if let data = tx.data { txDict["data"] = data }
                if let value = tx.value { txDict["value"] = value }

                let txResponse = try await portal.request(
                    chainId: "eip155:11155111",
                    method: .eth_sendTransaction,
                    params: [txDict],
                    options: RequestOptions(signatureApprovalMemo: "Transfer delegated tokens")
                )
                print("Tx \(index + 1) hash: \(txResponse.result as? String ?? "Unknown")")
            }
        }
    } catch {
        print("Error transferring EVM delegation: \(error)")
    }
}
```

### Solana Transfer From

```swift theme={null}
Task {
    do {
        let request = TransferFromRequest(
            chain: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
            token: "USDC",
            fromAddress: "ARttPLesu9RiX6H111Pfdc9Y2DhGy1B8P8jyyrD8Cj5b", // Token owner
            toAddress: "GPsPXxoQA51aTJJkNHtFDFYui5hN5UxcFPnheJEHa5Du",   // Recipient
            amount: "0.01"
        )

        let response = try await portal.delegations.transferFrom(request: request)

        // Sign and send Solana transactions sequentially
        if let encodedTransactions = response.encodedTransactions {
            for (index, encodedTx) in encodedTransactions.enumerated() {
                let txResponse = try await portal.request(
                    chainId: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
                    method: .sol_signAndSendTransaction,
                    params: [encodedTx],
                    options: RequestOptions(signatureApprovalMemo: "Transfer delegated tokens")
                )
                print("Tx \(index + 1) hash: \(txResponse.result as? String ?? "Unknown")")
            }
        }
    } catch {
        print("Error transferring Solana delegation: \(error)")
    }
}
```

<Note>
  **Delegation Roles**: `fromAddress` is the token owner who approved the delegation. Your wallet (the delegate) signs the transaction to transfer tokens from the owner to the `toAddress` recipient.
</Note>

***

## Supported Networks

Delegations work on all Portal-supported EVM and Solana chains:

* **EVM**: Ethereum, Polygon, Base, Arbitrum, Optimism, Monad, and all other EVM-compatible chains
* **Solana**: Solana Mainnet and Devnet

For a complete list, see [Blockchain Support](/resources/blockchain-support).

***

## Next Steps

* Read the reference pages for [approveAndSubmit](../reference/delegationsapproveandsubmit), [revokeAndSubmit](../reference/delegationsrevokeandsubmit), [transferAndSubmit](../reference/delegationstransferandsubmit), and [setSignAndSendTransaction](../reference/delegationssetsignandsendtransaction)
* Learn about [signing transactions](./sign-a-transaction)
* Explore [Portal API methods](./portal-api-methods)
* Review [delegation concepts](/resources/delegations)
* Check out [wallet lifecycle management](./manage-wallet-lifecycle-states)
