Rainbow Wallet provides secure transaction and message signing for dApps connected via WalletConnect.
Request Types
Rainbow supports multiple types of signing requests:
Personal Sign
Sign plain text messages:
src/walletConnect/index.tsx
case RPCMethod.PersonalSign: {
const [address, message] = params.sort(a => (isAddress(a) ? -1 : 1));
const isHex = isHexString(message);
let decodedMessage = message;
try {
if (isHex) {
decodedMessage = toUtf8String(message);
}
} catch (err) {
// Handle decode error
}
return {
address: getAddress(address),
message: decodedMessage,
};
}
Use cases:
- Login authentication
- Message verification
- Proof of ownership
Sign Typed Data
Sign structured data (EIP-712):
src/walletConnect/index.tsx
case RPCMethod.SignTypedData:
case RPCMethod.SignTypedDataV1:
case RPCMethod.SignTypedDataV3:
case RPCMethod.SignTypedDataV4: {
const [address, message] = params;
return {
address: getAddress(address),
message: JSON.parse(message),
};
}
Typed data includes:
- Domain separator
- Message types
- Structured values
- Primary type
Common use cases:
- Permit approvals (gasless ERC-20 approvals)
- Meta-transactions
- Order signing (Uniswap, OpenSea)
- Vote delegation
Send Transaction
Send Ethereum transactions:
src/walletConnect/index.tsx
case RPCMethod.SendTransaction: {
const [tx] = params;
return {
address: getAddress(tx.from),
};
}
Transaction fields:
from: Sender address
to: Recipient address
value: ETH amount
data: Contract call data
gas: Gas limit
gasPrice or maxFeePerGas: Gas pricing
Rainbow does NOT support eth_sign for security reasons. This legacy method can be exploited to sign arbitrary transactions.
Request Handling
When a dApp sends a request:
src/walletConnect/index.tsx
export async function onSessionRequest(
event: SignClientTypes.EventArguments['session_request']
) {
setHasPendingDeeplinkPendingRedirect(true);
const client = await getWalletKitClient();
const { id, topic } = event;
const { method: _method, params } = event.params.request;
const method = _method as RPCMethod;
if (method === RPCMethod.Sign) {
// Reject eth_sign for security
await client.respondSessionRequest({
topic,
response: formatJsonRpcError(id, `Rainbow does not support legacy eth_sign`),
});
showErrorSheet({
title: 'Request Failed',
body: 'eth_sign is not supported for security reasons.',
sheetHeight: 270,
});
return;
}
if (isSupportedMethod(method)) {
const { address, message } = parseRPCParams({ method, params });
// Build request display
const displayDetails = await getRequestDisplayDetails(
event.params.request,
nativeCurrency,
chainId
);
// Show signing sheet
handleWalletConnectRequest(request);
}
}
Receive Request
dApp sends a signing or transaction request via WalletConnect.
Validate Request
Rainbow validates the request method and parameters.
- Check if method is supported
- Verify wallet ownership
- Parse request parameters
- Check for read-only wallet
Display Request
Show the request details in a signing sheet.src/walletConnect/index.tsx
const request: WalletconnectRequestData = {
clientId: session.topic,
peerId: session.topic,
requestId: event.id,
dappName,
dappUrl: peerMeta.url,
displayDetails,
imageUrl: maybeSignUri(dappImage, { w: 200 }),
address,
chainId,
payload: event.params.request,
};
User Review
You review the request details and approve or reject.
Send Response
Rainbow sends the result back to the dApp.
Request Validation
Wallet Type Check
src/walletConnect/index.tsx
const selectedWallet = getWalletWithAccount(address);
const isReadOnly = selectedWallet?.type === WalletTypes.readOnly;
if (!selectedWallet || isReadOnly) {
await client.respondSessionRequest({
topic,
response: formatJsonRpcError(id, `Wallet is read-only`),
});
showErrorSheet({
title: 'Cannot Sign',
body: 'This wallet is read-only and cannot sign transactions.',
sheetHeight: 270,
});
return;
}
Validation checks:
- Wallet exists
- Wallet is not read-only
- Address matches session
- Chain is supported
Parameter Validation
src/walletConnect/index.tsx
if (!address || !message) {
await client.respondSessionRequest({
topic,
response: formatJsonRpcError(id, `Invalid RPC params`),
});
showErrorSheet({
title: 'Invalid Request',
body: 'The request has invalid or missing parameters.',
sheetHeight: 270,
});
return;
}
Signing Flow
Message Signing
Review Message
The message is displayed for your review.
- Plain text messages are shown as-is
- Hex messages are decoded to UTF-8 if possible
- Typed data is formatted with structure
Verify dApp
Check the dApp name and URL to confirm authenticity.
Sign or Reject
Tap approve to sign, or reject to decline.
Biometric Confirmation (Optional)
Authenticate with Face ID/Touch ID if enabled.
Transaction Signing
Review Transaction
Transaction details are displayed:
- To address
- Value (ETH amount)
- Contract interaction details
- Decoded function call (if available)
- Gas estimate
Check Gas Settings
Review and optionally adjust gas fees.
Approve or Reject
Confirm the transaction or reject it.
Sign & Broadcast
Rainbow signs and broadcasts the transaction to the network.
Response Handling
Successful Signing
src/walletConnect/index.tsx
export async function handleSessionRequestResponse(
{ sessionRequestEvent }: { sessionRequestEvent: SignClientTypes.EventArguments['session_request'] },
{ result, error }: { result: string | null; error: any }
) {
const client = await getWalletKitClient();
const { topic, id } = sessionRequestEvent;
if (result) {
const payload = {
topic,
response: formatJsonRpcResult(id, result),
};
await client.respondSessionRequest(payload);
} else {
const payload = {
topic,
response: formatJsonRpcError(id, error),
};
await client.respondSessionRequest(payload);
}
removeWalletConnectRequest({ walletConnectRequestId: sessionRequestEvent.id });
}
Success flow:
- User approves request
- Rainbow signs the message/transaction
- Result sent to dApp via WalletConnect
- Request removed from queue
- Success notification shown
Rejected Requests
Rejection flow:
- User rejects request
- Error response sent to dApp
- Request removed from queue
- User returned to previous screen
Hardware Wallet Support
Signing with hardware wallets (Ledger):
src/walletConnect/index.tsx
const authenticate: AuthRequestAuthenticateSignature = async ({ address }) => {
const selectedWallet = getWalletWithAccount(address);
const isHardwareWallet = selectedWallet?.type === WalletTypes.bluetooth;
const signature = await (isHardwareWallet
? new Promise<string>((y, n) => {
Navigation.handleAction(Routes.HARDWARE_WALLET_TX_NAVIGATOR, {
async submit() {
try {
y(await loadWalletAndSignMessage());
} catch (e) {
n(e);
}
},
});
})
: loadWalletAndSignMessage());
return signature;
};
Detect Hardware Wallet
Rainbow detects if the connected wallet is a Ledger device.
Navigate to Ledger Flow
User is guided through the hardware wallet signing flow.
Approve on Device
User confirms the signature on the Ledger device.
Return Signature
Signature is returned and sent to the dApp.
Sign-In With Ethereum (SIWE)
Rainbow supports WalletConnect’s authentication feature:
src/walletConnect/sheets/AuthRequest.tsx
export function AuthRequest({
requesterMeta,
authenticate,
verifiedData,
}: {
requesterMeta: WalletKitTypes.SessionProposal['params']['proposer']['metadata'];
authenticate: AuthRequestAuthenticateSignature;
verifiedData?: Verify.Context['verified'];
}) {
const auth = React.useCallback(async () => {
const { success, reason } = await authenticate({ address });
if (!success) {
switch (reason) {
case AuthRequestResponseErrorReason.ReadOnly:
Alert({
message: 'Please switch to a different wallet to sign this request',
title: 'Cannot sign with this wallet',
});
break;
default:
Alert({
title: 'Authentication Failed',
message: 'An error occurred while authenticating.',
});
}
} else {
goBack();
}
}, [address, authenticate, goBack]);
}
SIWE use cases:
- One-click dApp login
- Gasless authentication
- Session management
- Proof of wallet ownership
SIWE provides a standardized way to authenticate using your Ethereum wallet, similar to “Sign in with Google”.
Request Queue
Multiple requests are queued:
src/state/walletConnectRequests.ts
export function addNewWalletConnectRequest({
walletConnectRequest,
}: {
walletConnectRequest: WalletconnectRequestData;
}): boolean {
const state = getWalletConnectRequestsStore();
// Check if request already exists
if (state.requests.some(r => r.requestId === walletConnectRequest.requestId)) {
return false;
}
// Add to queue
state.requests.push(walletConnectRequest);
return true;
}
export function removeWalletConnectRequest({
walletConnectRequestId,
}: {
walletConnectRequestId: number;
}) {
const state = getWalletConnectRequestsStore();
state.requests = state.requests.filter(
r => r.requestId !== walletConnectRequestId
);
}
Queue behavior:
- Requests shown one at a time
- FIFO (first in, first out) order
- Each request must be approved or rejected
- Queue persists across app restarts
Post-Request Redirect
After completing a request, redirect back to dApp:
src/walletConnect/index.tsx
const request: WalletconnectRequestData = {
// ...
walletConnectV2RequestValues: {
sessionRequestEvent: event,
address,
chainId,
onComplete(type: WalletconnectResultType) {
if (IS_IOS) {
Navigation.handleAction(Routes.WALLET_CONNECT_REDIRECT_SHEET, {
type,
});
}
maybeGoBackAndClearHasPendingRedirect({ delay: 300 });
},
},
};
Redirect types:
- sign: Message signed
- transaction: Transaction sent
- reject: Request rejected
The redirect ensures you’re automatically returned to the dApp after signing, creating a seamless mobile experience.
Analytics
Signing events are tracked:
src/walletConnect/index.tsx
analytics.track(analytics.event.wcShowingSigningRequest, {
dappName: request.dappName,
dappUrl: request.dappUrl,
});
analytics.track(analytics.event.wcRequestFailed, {
type: 'session_request',
reason: 'read only wallet',
});
Tracked events:
- Signing requests shown
- Requests approved
- Requests rejected
- Request failures
- Hardware wallet usage