smartAccount, client (Web SDK), bundlerClient
(iOS, Android), and modularTransport (iOS, Android) returned by
Create a modular wallet.
Sign and verify a plain message
UsesignMessage 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
UsesignTypedData 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();
}