# Empowering Dapps: The Ultimate Handbook for Smart Wallet Adoption
As Ethereum transitions from Externally Owned Accounts (EOAs) to smart accounts, the potential for actually bringing an improved experience of Dapps to average users becomes increasingly evident.
As the adoption of account abstraction through ERC-4337 gains momentum, a renewed push is underway to enhance the user experience of web3. However, despite the shared goal of advancing both AA and user experience, Dapps have somewhat lagged behind, posing a challenge to facilitating a meaningful transition.
In this handbook, we cast a spotlight on three core domains for dapps to adopt smart wallets and to unleash the complete potential of their interfaces:
1. **Enable Smart Wallet Connection**: Using WalletConnect and Injected Wallets providers
2. **Leverage Smart Wallet Features**: improve UX and onboarding
3. **Required standards to adopt**: Differences with EOAs: signature verification, and tracking transaction status
## Enable Smart Wallet Connection
### WalletConnect
Smart Contract wallets are fully supported by the WalletConnect protocol.
However, there are some considerations to be taken when integrating WalletConnect in your dapp for Smart Contract wallets. WalletConnect V2 allows setting specific chains and methods to be specified as required or optional via session namespaces.
Because smart contract wallets may be not deployed on all chains, but deployed on optional chains, you might break the connection if you set a required chain or method in the configuration.
#### Recommended Config
##### Required and Optional Methods
By default, EthereumProvider specifies `eth_sendTransaction` and `personal_sign` as required methods. For those that want to request additional methods for the session, we recommend passing these through optionalMethods.
#### Using @walletconnect/ethereum-provider
```typescript
import { EthereumProvider } from '@walletconnect/ethereum-provider'
const provider = await EthereumProvider.init({
...
optionalMethods: ["eth_signTypedData"],
...
})
```
#### Using @web3-onboard/walletconnect library
```typescript
import walletConnectModule from "@web3-onboard/walletconnect";
const wcV2InitOptions = {
...
additionalOptionalMethods: ["eth_signTypedData"],
...
};
const walletConnect = walletConnectModule(wcV2InitOptions);
```
##### Required and Optional Chains
The recommended config depends on how you designed your interface when a user first connects their wallet.
1. **You let users select the chain before they connect their wallet**
Set the required chain id to match the one that the user selected. Pass the rest of the chains your dapp support in the optional field.
#### Using @walletconnect/ethereum-provider
```typescript
import { EthereumProvider } from '@walletconnect/ethereum-provider'
const provider = await EthereumProvider.init({
...
})
provider.connect({
chains: [10], // user selected optimism
optionalChains: [1, 137]
});
```
#### Using @web3-onboard/walletconnect library
```typescript
import walletConnectModule from "@web3-onboard/walletconnect";
const wcV2InitOptions = {
...
requiredChains: [137], // user selected polygon
optionalChains: [1, 10],
...
};
const walletConnect = walletConnectModule(wcV2InitOptions);
```
#### Using Web3Modal & Wagmi
```typescript
import { connect } from '@wagmi/core'
import { optimism } from '@wagmi/core/chains'
import { WalletConnectConnector } from '@wagmi/core/connectors/walletConnect'
const connector = new WalletConnectConnector({
options: {
projectId: '...',
},
})
const result = await connect({
chainId: optimism.id,
connector,
})
```
Source: https://wagmi.sh/core/actions/connect
2. **You let users connect their wallet before they select the chain**
Pass all chains your dapp support in optional field.
#### Using @walletconnect/ethereum-provider
```typescript
import { EthereumProvider } from '@walletconnect/ethereum-provider'
const provider = await EthereumProvider.init({
...
optionalChains: [1, 10],
...
})
```
#### Using @web3-onboard/walletconnect library
```typescript
import walletConnectModule from "@web3-onboard/walletconnect";
const wcV2InitOptions = {
...
optionalChains: [1, 10],
...
};
const walletConnect = walletConnectModule(wcV2InitOptions);
```
#### Using Web3Modal & Wagmi (WIP)
Web3Modal and Wagmi currently do no support passing in an empty chains array to EthereumProvider.init.
However, you can show the network selector view and let the user choose the network they want to connect to. They will pass the network as a required chain for the wallet to support.
```typescript
import { useWeb3Modal } from '@web3modal/react'
const { open } = useWeb3Modal();
open({ route: 'SelectNetwork' });
```
Source: https://docs.walletconnect.com/2.0/web3modal/react/wagmi/hooks#useweb3modal
#### Testing
Use the *walletconnect required namespace* (`wc-rns`) tool to test your required namespace communication. Just paste a walletconnect project id, and the uri from your dapp. `wc-rns` will then display the required namespaces.
https://wc-rns.surge.sh/
##### Documentation Resources
[Required and Optional Chains](https://docs.walletconnect.com/2.0/advanced/providers/ethereum#required-and-optional-chains)
[Required and Optional Methods](https://docs.walletconnect.com/2.0/advanced/providers/ethereum#required-and-optional-methods)
## Leverage Smart Wallet Features
### Batching
> Batched transactions allow you to perform multiple transactions in one single on-chain transaction.
>
If you are building a dapp, you communicate the actions to the wallet that the user performs. Instead of sending a single transaction one after the other, you can send an array of transactions, and the smart wallet will execute the batch. This greatly improves the user experience and allows for one click interfaces.
Each wallet has one way or the other to execute batch transactions. Safe uses the multicall contract to batch, and other wallets might use different methods.
[EIP-5792](https://eips.ethereum.org/EIPS/eip-5792) allows us to standardise JSON-RPC methods for Dapps to communicate bundle calls to wallets using:
- wallet_sendFunctionCallBundle
- wallet_getBundleStatus
- wallet_showBundleStatus
A simple example:
```json
{
"jsonrpc": "2.0",
"id": 0,
"method": "wallet_sendFunctionCallBundle",
"params": [
{
"chainId": 1,
"from": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"calls": [
{
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"gas": "0x76c0",
"value": "0x9184e72a",
"data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"
},
{
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"gas": "0xdefa",
"value": "0x182183",
"data": "0xfbadbaf01"
}
]
}
]
}
```
For more complete example: checkout this guide on [how to leverage batch transactions for your dapp](https://docs.candide.dev/dapps/guides/batch-transactions).
### Sponsoring
> Sponsored transactions give your dapp the ability to pay for users' transaction fees.
#### Overview
Without sponsored transactions, anyone who sends an Ethereum transaction needs to have ETH to pay for gas fees. This forces new users to pass KYC and purchase ETH before they can start using your dapp. This can be a major hurdle for users without prior crypto experience that are unfamiliar with the concept of needing to keep ETH in their wallet for gas.
Thanks to smart wallets, your users can now interact with Ethereum smart contracts without needing ETH for transaction fees.
Example of sponsored transactions:
- Onboarding: Enable a frictionless onboarding process for new users
- Pay for gas indirectly: Allow users to pay for gas indirectly via a purchase on your dapp with stablecoins
- Privacy: Enabling ETH-less withdrawal of tokens sent to stealth addresses
#### Two Ways to allow for sponsored transactions:
1. **Partner with Paymasters**
Many wallets rely on 3rd party [paymaster](https://eips.ethereum.org/EIPS/eip-4337#extension-paymasters) services, or equivilant, for sponsored transactions. By partnering with a paymaster provider, you can enable sponsored transactions with all participating wallets.
2. **Partner with Smart Wallets**
You can partner directly with individual smart wallets based on your usecase to allow for sponsored transactions on your dapp.
## Required Standards To Adopt
### Signature Verification
Normally, when verifying signatures from regular accounts, which are Externally Owned Accounts (EOAs), you would use an ECDSA method called ecrecover() to retrieve the corresponding public key, which will then map to an address.
In the case of Smart Contract Wallets, you are not able to sign a message with the smart contract account.
Therefore, the recommended way is to check for smart contract wallet signatures is using ERC-6492, which extends ERC-1271. The reasons to use it:
- It adds support for undeployed contracts too
- It is a single `eth_call()` to do all checks: either ecrecover, `isValidSignature` on existing contract or `isValidSignature` on pre-deployed contract.
#### ERC-6492
https://eips.ethereum.org/EIPS/eip-6492
You can verify whether a signature on behalf of a given counterfactual contract (even if not deployed yet) is valid. This standard extends ERC-1271.
The first argument is the address of the account, the second is the `_hash` argument which accepts the hash of the message digest, and the third argument `_signature` is the signed payload returned by the wallet upon signing.
##### Using [signature-validator](https://github.com/AmbireTech/signature-validator/) (recommended)
```typescript
import ethers from 'ethers';
import { verifyMessage } from '@ambire/signature-validator';
const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com')
async function run() {
// Replace `ethers.verifyMessage(message, signature) === signer` with this:
const isValidSig = await verifyMessage({
signer: '0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9',
message: 'My funds are SAFU with Ambire Wallet',
signature: '0x9863d84f3119ac01d9e3bf9294e6c0c3572a07780fc7c49e8dc913806f4b1dbd4cc075462dc84422a9b981b2556f9c9197d76da7ba3603e53e9300869c574d821c',
// this is needed so that smart contract signatures can be verified
provider,
})
console.log('is the sig valid: ', isValidSig)
}
run().catch(e => console.error(e))
```
##### Using ethers library:
```typescript
const isValidSignature = '0x01' === await provider.call({
data: ethers.utils.concat([
validateSigOffchainBytecode,
(new ethers.utils.AbiCoder()).encode(['address', 'bytes32', 'bytes'], [signer, hash, signature])
])
})
```
You can also take a reference from the following [ethSign_6492.js](https://github.com/AmbireTech/signature-validator/blob/main/tests/6_ethSign_6492.js) test.
### Don't use `tx.origin`
**`tx.origin`**: This represents the original external user (i.e., an externally owned account or an externally deployed contract) that initiated the transaction. It is the very first sender of the transaction, even if the transaction was forwarded through multiple contracts. It's important to note that this can change if the transaction is forwarded through multiple contracts, and it can lead to unexpected behavior if used in certain situations.
#### Smart Contracts
**`tx.origin`** breaks compatibility. Using it means that your contract cannot be used by another contract, because a contract can never be the tx.origin. It makes smart contract wallets incompatible with your contract if you assume it is equal to `msg.sender`.
#### Tracking Transaction Status
Don't assume that `tx.origin` will be the one executing the transaction on chain. Smart Contract wallets commonly use relayer. ERC-4337 utilises a Bundlers network to sign and submit user operations onchains. Others might use a private relayer to do so.
Your dapp will receive the transaction hash in order to monitor the status of the transaction, and events will be emitted as usual. The "from" of this transaction will change, and that's execpted behaviour. Don't do like [uniswap](https://github.com/Uniswap/interface/issues/5776#issuecomment-1542192480)..
## Extras
### 1. Detecting a Smart Wallet
If you followed this guide, there's no real reason why you should need to detect whether the wallet connected is a smart wallet or not. However, if you really need: you can detect smart contract wallets by verifying on-chain if the exposed account address has any associated code deployed.
Using etherjs
```typescript
import { providers, utils } from 'ethers'
const provider = new providers.JsonRpcProvider(rpcUrl)
const bytecode = await provider.getCode(address)
const isSmartContract = bytecode && utils.hexStripZeros(bytecode) !== '0x'
```
Using web3js
```typescript
import Web3 from 'web3'
const web3 = new Web3(rpcUrl)
const bytecode = await web3.eth.getCode(address)
const isSmartContract = bytecode && utils.hexStripZeros(bytecode) !== '0x'
```
Using viem
```typescript
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
export const publicClient = createPublicClient({
chain: mainnet,
transport: http()
});
const bytecode = await publicClient.getBytecode({
address,
});
const isSmartContract = bytecode && utils.hexStripZeros(bytecode) !== '0x'
```
:::info
While this method effectively identifies smart contract wallets with deployed code, it's essential to be aware that this approach does not account for undeployed smart contract wallets
:::