Skip to main content
Modular wallets sign offchain messages for Sign-In With Ethereum (SIWE) authentication, EIP-712 typed data for offchain orders, and other flows where your backend or a contract needs to prove the user authorized the data. Plain messages and EIP-712 typed data are two independent flows. The examples below use the smartAccount, client (Web SDK), bundlerClient (iOS, Android), and modularTransport (iOS, Android) returned by Create a modular wallet.

Sign and verify a plain message

Use signMessage for plain strings, such as SIWE messages or human-readable consent strings. The Web SDK verifies through viem’s verifyMessage. iOS and Android verify by calling isValidSignature on the smart account contract directly.
const message = "Sign in to MyApp at 2026-06-24T20:00:00Z";

const signature = await smartAccount.signMessage({ message });

const isValid = await client.verifyMessage({
  address: smartAccount.address,
  message,
  signature,
});
Task {
    do {
        let message = "Sign in to MyApp at 2026-06-24T20:00:00Z"

        let signature = smartAccount.signMessage(message)

        // Smart account signatures verify via EIP-1271. The contract
        // returns EIP1271_VALID_SIGNATURE (0x1626ba7e) when valid.
        let digest = Data(message.utf8).sha3(.keccak256)
        let function = ABI.Element.Function(
            name: "isValidSignature",
            inputs: [
                ABI.Element.InOut(name: "digest", type: .bytes(length: 32)),
                ABI.Element.InOut(name: "signature", type: .dynamicBytes),
            ],
            outputs: [ABI.Element.InOut(name: "magicValue", type: .bytes(length: 4))],
            constant: false,
            payable: false
        )
        guard let data = function.encodeParameters([digest, signature] as [Any]) else {
            return
        }

        guard let toAddress = EthereumAddress(smartAccount.address) else { return }
        let transaction = CodableTransaction(to: toAddress, data: data)

        guard let callResult = try? await Utils().ethCall(
            transport: modularTransport,
            transaction: transaction
        ),
        let callResultData = hexToData(hex: callResult),
        let decoded = try? function.decodeReturnData(callResultData),
        let magicValue = decoded["0"] as? Data else {
            return
        }

        let isValid = (EIP1271_VALID_SIGNATURE == magicValue.bytes)
    } catch {
        print(error)
    }
}
CoroutineScope(Dispatchers.IO).launch {
    try {
        val message = "Sign in to MyApp at 2026-06-24T20:00:00Z"

        val signature = smartAccount.signMessage(context, message)

        // Smart account signatures verify via EIP-1271. The contract
        // returns EIP1271_VALID_SIGNATURE (0x1626ba7e) when valid.
        val digest = toSha3Bytes(hashMessage(message.toByteArray()))
        val function = Function(
            "isValidSignature",
            listOf<Type<*>>(
                Bytes32(digest),
                DynamicBytes(Numeric.hexStringToByteArray(signature)),
            ),
            listOf<TypeReference<*>>(object : TypeReference<Bytes4>() {}),
        )
        val data = FunctionEncoder.encode(function)
        val resp = bundlerClient.call(
            smartAccount.getAddress(),
            CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.address,
            data,
        )
        val decoded = FunctionReturnDecoder.decode(resp, function.outputParameters)
        val isValid = EIP1271_VALID_SIGNATURE.contentEquals(decoded[0].value as ByteArray)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
try {
    String message = "Sign in to MyApp at 2026-06-24T20:00:00Z";

    CompletableFuture<String> signatureFuture = new CompletableFuture<>();
    smartAccount.signMessage(context, message, new CustomContinuation<>(signatureFuture));
    String signature = signatureFuture.join();

    // Smart account signatures verify via EIP-1271. The contract returns
    // EIP1271_VALID_SIGNATURE (0x1626ba7e) when valid.
    byte[] digest = toSha3Bytes(hashMessage(message.getBytes(StandardCharsets.UTF_8)));
    Function function = new Function(
        "isValidSignature",
        Arrays.asList(
            new Bytes32(digest),
            new DynamicBytes(Numeric.hexStringToByteArray(signature))
        ),
        Arrays.asList(new TypeReference<Bytes4>() {})
    );
    String data = FunctionEncoder.encode(function);

    CompletableFuture<String> callFuture = new CompletableFuture<>();
    bundlerClient.call(
        smartAccount.getAddress(),
        CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.INSTANCE.getAddress(),
        data,
        new CustomContinuation<>(callFuture)
    );
    String resp = callFuture.join();

    List<Type> decoded = FunctionReturnDecoder.decode(resp, function.getOutputParameters());
    boolean isValid = Arrays.equals(
        MscaConstantsKt.getEIP1271_VALID_SIGNATURE(),
        ((Bytes4) decoded.get(0)).getValue()
    );
} catch (Exception e) {
    e.printStackTrace();
}

Sign and verify EIP-712 typed data

Use signTypedData for structured data. Define the domain, types, and message, and the SDK produces an EIP-712 signature your contracts and backend can verify.
const typedData = {
  domain: {
    name: "MyApp",
    version: "1",
    chainId: 421614,
    verifyingContract: "0x...",
  },
  types: {
    Order: [
      { name: "maker", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "expiry", type: "uint256" },
    ],
  },
  primaryType: "Order",
  message: {
    maker: smartAccount.address,
    amount: 1_000_000n,
    expiry: BigInt(Math.floor(Date.now() / 1000) + 3600),
  },
} as const;

const signature = await smartAccount.signTypedData(typedData);

const isValid = await client.verifyTypedData({
  address: smartAccount.address,
  signature,
  ...typedData,
});
Task {
    do {
        let signature = try await smartAccount.signTypedData(typedData: typedData)
        // Verify by calling isValidSignature on the smart account contract,
        // using the EIP-712 digest of the typed data as the digest input.
    } catch {
        print(error)
    }
}
CoroutineScope(Dispatchers.IO).launch {
    try {
        val signature = smartAccount.signTypedData(context, typedData)
        // Verify by calling isValidSignature on the smart account contract,
        // using the EIP-712 digest of the typed data as the digest input.
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
try {
    CompletableFuture<String> signatureFuture = new CompletableFuture<>();
    smartAccount.signTypedData(context, typedData, new CustomContinuation<>(signatureFuture));
    String signature = signatureFuture.join();
    // Verify by calling isValidSignature on the smart account contract,
    // using the EIP-712 digest of the typed data as the digest input.
} catch (Exception e) {
    e.printStackTrace();
}