Skip to main content

Overview

This guide covers how to interact with smart contracts on EVM chains, including reading contract state and executing contract functions.

Prerequisites

Write to Contract

Execute state-changing functions on a smart contract:
import DynamicSDKSwift

let sdk = DynamicSDK.instance()

func writeContract(wallet: BaseWallet) async {
    // Example: Calling a custom contract function
    let customAbi = """
    [
        {
            "inputs": [
                {"name": "value", "type": "uint256"}
            ],
            "name": "setValue",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        }
    ]
    """

    do {
        // Parse ABI JSON string to array
        guard let abiData = customAbi.data(using: .utf8),
              let abiArray = try JSONSerialization.jsonObject(with: abiData) as? [[String: Any]] else {
            throw DynamicSDKError.custom("Invalid ABI format")
        }

        let input = WriteContractInput(
            address: "0xYourContractAddress",
            abi: abiArray,
            functionName: "setValue",
            args: ["12345"]
        )

        let txHash = try await sdk.evm.writeContract(
            wallet: wallet,
            input: input
        )

        print("Contract interaction successful!")
        print("Hash: \(txHash)")
    } catch {
        print("Contract call failed: \(error)")
    }
}

Read Contract Data

Query contract state without sending a transaction:
import DynamicSDKSwift

let sdk = DynamicSDK.instance()

func readContract(chainId: Int) async {
    let contractAbi = """
    [
        {
            "inputs": [],
            "name": "getValue",
            "outputs": [
                {"name": "", "type": "uint256"}
            ],
            "stateMutability": "view",
            "type": "function"
        }
    ]
    """

    do {
        let client = sdk.evm.createPublicClient(chainId: chainId)

        let result = try await client.readContract(
            address: "0xYourContractAddress",
            functionName: "getValue",
            abi: contractAbi
        )

        print("Contract value: \(result)")
    } catch {
        print("Failed to read contract: \(error)")
    }
}

Complete Contract Interaction Example

import SwiftUI
import DynamicSDKSwift

struct ContractInteractionView: View {
    let wallet: BaseWallet
    let contractAddress: String
    let chainId: Int

    @State private var inputValue = ""
    @State private var currentValue: String?
    @State private var txHash: String?
    @State private var isLoading = false
    @State private var error: String?

    private let sdk = DynamicSDK.instance()

    // Example ABI for a simple storage contract
    private let contractAbi = """
    [
        {
            "inputs": [{"name": "value", "type": "uint256"}],
            "name": "setValue",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "getValue",
            "outputs": [{"name": "", "type": "uint256"}],
            "stateMutability": "view",
            "type": "function"
        }
    ]
    """

    var body: some View {
        VStack(spacing: 20) {
            // Read section
            VStack(alignment: .leading, spacing: 8) {
                Text("Read Contract")
                    .font(.headline)

                if let value = currentValue {
                    HStack {
                        Text("Current Value:")
                        Text(value)
                            .fontWeight(.bold)
                    }
                }

                Button("Refresh Value") {
                    Task { await readValue() }
                }
                .buttonStyle(.bordered)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
            .background(Color(.systemGray6))
            .cornerRadius(8)

            Divider()

            // Write section
            VStack(alignment: .leading, spacing: 8) {
                Text("Write to Contract")
                    .font(.headline)

                TextField("New value", text: $inputValue)
                    .textFieldStyle(.roundedBorder)
                    .keyboardType(.numberPad)

                Button("Set Value") {
                    Task { await setValue() }
                }
                .buttonStyle(.borderedProminent)
                .disabled(inputValue.isEmpty || isLoading)

                if isLoading {
                    ProgressView("Processing...")
                }

                if let hash = txHash {
                    VStack(alignment: .leading, spacing: 4) {
                        Text("Transaction sent!")
                            .foregroundColor(.green)
                        Text("Hash: \(hash)")
                            .font(.caption)
                            .lineLimit(1)
                    }
                }

                if let error = error {
                    Text(error)
                        .foregroundColor(.red)
                        .font(.caption)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
            .background(Color(.systemGray6))
            .cornerRadius(8)
        }
        .padding()
        .onAppear {
            Task { await readValue() }
        }
    }

    private func readValue() async {
        do {
            let client = sdk.evm.createPublicClient(chainId: chainId)
            let result = try await client.readContract(
                address: contractAddress,
                functionName: "getValue",
                abi: contractAbi
            )
            await MainActor.run {
                currentValue = String(describing: result)
            }
        } catch {
            await MainActor.run {
                self.error = "Failed to read: \(error.localizedDescription)"
            }
        }
    }

    private func setValue() async {
        await MainActor.run {
            isLoading = true
            error = nil
            txHash = nil
        }

        do {
            // Parse ABI JSON string to array
            guard let abiData = contractAbi.data(using: .utf8),
                  let abiArray = try JSONSerialization.jsonObject(with: abiData) as? [[String: Any]] else {
                throw NSError(domain: "ContractError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid ABI format"])
            }

            let input = WriteContractInput(
                address: contractAddress,
                abi: abiArray,
                functionName: "setValue",
                args: [inputValue]
            )

            let hash = try await sdk.evm.writeContract(
                wallet: wallet,
                input: input
            )

            await MainActor.run {
                txHash = hash
                // Refresh the value after transaction
                Task { await readValue() }
            }
        } catch {
            await MainActor.run {
                self.error = "Failed to set value: \(error.localizedDescription)"
            }
        }

        await MainActor.run {
            isLoading = false
        }
    }
}

Working with Contract ABIs

Defining ABIs

ABIs (Application Binary Interfaces) define how to interact with smart contracts:
// Simple storage contract ABI
let storageAbi = """
[
    {
        "inputs": [{"name": "value", "type": "uint256"}],
        "name": "store",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "retrieve",
        "outputs": [{"name": "", "type": "uint256"}],
        "stateMutability": "view",
        "type": "function"
    }
]
"""

// ERC-721 NFT contract functions
let nftAbi = """
[
    {
        "inputs": [{"name": "to", "type": "address"}, {"name": "tokenId", "type": "uint256"}],
        "name": "mint",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [{"name": "owner", "type": "address"}],
        "name": "balanceOf",
        "outputs": [{"name": "", "type": "uint256"}],
        "stateMutability": "view",
        "type": "function"
    }
]
"""

Built-in ABIs

The SDK includes common contract ABIs. Note that you need to parse the ABI string to a JSON array:
// Parse the built-in ERC-20 ABI
guard let abiData = Erc20.abi.data(using: .utf8),
      let abiArray = try JSONSerialization.jsonObject(with: abiData) as? [[String: Any]] else {
    throw DynamicSDKError.custom("Invalid Erc20 ABI")
}

// ERC-20 token transfers
let input = WriteContractInput(
    address: tokenAddress,
    abi: abiArray,
    functionName: "transfer",
    args: [recipient, amount]
)

Advanced Contract Interactions

NFT Minting

func mintNFT(
    wallet: BaseWallet,
    nftContractAddress: String,
    recipient: String,
    tokenId: String
) async throws -> String {
    let nftAbi = """
    [
        {
            "inputs": [
                {"name": "to", "type": "address"},
                {"name": "tokenId", "type": "uint256"}
            ],
            "name": "mint",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        }
    ]
    """

    // Parse ABI JSON string to array
    guard let abiData = nftAbi.data(using: .utf8),
          let abiArray = try JSONSerialization.jsonObject(with: abiData) as? [[String: Any]] else {
        throw DynamicSDKError.custom("Invalid ABI format")
    }

    let input = WriteContractInput(
        address: nftContractAddress,
        abi: abiArray,
        functionName: "mint",
        args: [recipient, tokenId]
    )

    return try await sdk.evm.writeContract(
        wallet: wallet,
        input: input
    )
}

Approve Token Spending

Before a contract can spend your tokens, you need to approve it:
func approveTokenSpending(
    wallet: BaseWallet,
    tokenAddress: String,
    spenderAddress: String,
    amount: String
) async throws -> String {
    // Parse the built-in ERC-20 ABI
    guard let abiData = Erc20.abi.data(using: .utf8),
          let abiArray = try JSONSerialization.jsonObject(with: abiData) as? [[String: Any]] else {
        throw DynamicSDKError.custom("Invalid Erc20 ABI")
    }

    let input = WriteContractInput(
        address: tokenAddress,
        abi: abiArray,
        functionName: "approve",
        args: [spenderAddress, amount]
    )

    return try await sdk.evm.writeContract(
        wallet: wallet,
        input: input
    )
}

Check Token Allowance

func checkAllowance(
    tokenAddress: String,
    ownerAddress: String,
    spenderAddress: String,
    chainId: Int
) async throws -> String {
    let client = sdk.evm.createPublicClient(chainId: chainId)

    let result = try await client.readContract(
        address: tokenAddress,
        functionName: "allowance",
        abi: Erc20.abi,
        args: [ownerAddress, spenderAddress]
    )

    return String(describing: result)
}

Best Practices

1. Validate Contract Addresses

func isValidContractAddress(_ address: String, chainId: Int) async -> Bool {
    do {
        let client = sdk.evm.createPublicClient(chainId: chainId)
        let code = try await client.getCode(address: address)
        return !code.isEmpty && code != "0x"
    } catch {
        return false
    }
}

2. Handle Contract Errors

do {
    let txHash = try await sdk.evm.writeContract(wallet: wallet, input: input)
} catch {
    let errorDesc = error.localizedDescription.lowercased()

    if errorDesc.contains("revert") {
        print("Contract reverted. Check contract requirements.")
    } else if errorDesc.contains("gas") {
        print("Out of gas. Increase gas limit.")
    } else {
        print("Contract call failed: \(error)")
    }
}

3. Estimate Gas for Complex Operations

// For complex contract interactions, estimate gas first
let estimatedGas = try await client.estimateGas(transaction)
let gasLimitWithBuffer = Int(Double(estimatedGas) * 1.2) // Add 20% buffer

Common Use Cases

DeFi: Swap Tokens

// Example: Uniswap token swap
func swapTokens(
    wallet: BaseWallet,
    routerAddress: String,
    amountIn: String,
    amountOutMin: String,
    path: [String],
    deadline: String
) async throws -> String {
    let swapAbi = """
    [
        {
            "inputs": [
                {"name": "amountIn", "type": "uint256"},
                {"name": "amountOutMin", "type": "uint256"},
                {"name": "path", "type": "address[]"},
                {"name": "to", "type": "address"},
                {"name": "deadline", "type": "uint256"}
            ],
            "name": "swapExactTokensForTokens",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        }
    ]
    """

    // Parse ABI JSON string to array
    guard let abiData = swapAbi.data(using: .utf8),
          let abiArray = try JSONSerialization.jsonObject(with: abiData) as? [[String: Any]] else {
        throw DynamicSDKError.custom("Invalid ABI format")
    }

    let input = WriteContractInput(
        address: routerAddress,
        abi: abiArray,
        functionName: "swapExactTokensForTokens",
        args: [amountIn, amountOutMin, path, wallet.address, deadline]
    )

    return try await sdk.evm.writeContract(wallet: wallet, input: input)
}

NFT: Check Ownership

func checkNFTOwnership(
    nftContractAddress: String,
    tokenId: String,
    chainId: Int
) async throws -> String {
    let nftAbi = """
    [
        {
            "inputs": [{"name": "tokenId", "type": "uint256"}],
            "name": "ownerOf",
            "outputs": [{"name": "", "type": "address"}],
            "stateMutability": "view",
            "type": "function"
        }
    ]
    """

    let client = sdk.evm.createPublicClient(chainId: chainId)
    let owner = try await client.readContract(
        address: nftContractAddress,
        functionName: "ownerOf",
        abi: nftAbi,
        args: [tokenId]
    )

    return String(describing: owner)
}

Next Steps