Integrating Notus with Metamask
In this guide, we’ll walk you through integrating Notus API with Metamask using viem and Next.js. By the end, you’ll be able to connect Metamask, interact with smart wallets, generate swap quotes, and execute User Operations.
Eager to get started? Check out our GitHub repository for a complete example and see how to integrate Notus API into your project.
In this guide, we’ll walk you through executing swaps, transactions, and other powerful features using Account Abstraction. If you’re unfamiliar with concepts like UserOperations, paymasters, or bundlers, explore these resources:
1. Start a Next.js ProjectCopied!
First, create a new Next.js project. Run the following command in your terminal:
npx create-next-app@latest
This will scaffold a new Next.js application, setting up a basic React-based environment for building your dApp.
2. Install viem
Copied!
We’ll use viem
, a library for blockchain interactions, to simplify our Metamask integration. Install it by running:
npm i viem
3. Connect Metamask with viem
Copied!
Now, let’s set up a connection to Metamask. This step allows users to connect their wallets and retrieve their account addresses. Add the following code to your App
component:
"use client";
import { createWalletClient, custom } from "viem";
import { polygon } from "viem/chains";
export default function App() {
const [externallyOwnedAccount, setExternallyOwnedAccount] = useState("");
const account = createWalletClient({
chain: polygon,
transport: custom(window.ethereum!),
});
const requestAddress = async () => {
const [externallyOwnedAccount] = await account.requestAddresses();
if (externallyOwnedAccount) {
setExternallyOwnedAccount(externallyOwnedAccount);
}
};
return (
<div>
<button onClick={async () => await requestAddress()}>Metamask</button>
</div>
);
}
Explanation:
-
createWalletClient
initializes the wallet connection. -
requestAddresses
fetches the connected wallet's account address. -
The address is stored in the
externallyOwnedAccount
state for future use.
4. Register or Query a Smart WalletCopied!
Using the externally owned account (EOA) retrieved in the previous step, we can register or fetch the corresponding smart wallet details (Account Abstraction).
"use client";
//...//
export default function App() {
const [externallyOwnedAccount, setExternallyOwnedAccount] = useState("");
const [accountAbstraction, setAccountAbstraction] = useState("");
//.....//
const getSmartWalletAddress = async () => {
const FACTORY_ADDRESS = "0x0000000000400CdFef5E2714E63d8040b700BC24";
let res = await fetch(`https://<baseUrl>/api/v1/wallets/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "<api-key>",
},
body: JSON.stringify({
externallyOwnedAccount: externallyOwnedAccount,
factory: FACTORY_ADDRESS,
salt: "0",
}),
});
if (!res?.ok) {
res = await fetch(
`https://<baseUrl>/api/v1/wallets/address?externallyOwnedAccount=${externallyOwnedAccount}&factory=${FACTORY_ADDRESS}&salt=${0}`,
{
method: "GET",
headers: {
"x-api-key": "<api-key>",
},
}
);
}
const data = await res.json();
setAccountAbstraction(data.wallet.accountAbstraction);
};
return (
<button onClick={() => getSmartWalletAddress()}>Get SmartWallet</button>
);
}
A Factory is a smart contract responsible for creating smart wallets. It helps ensure that wallet addresses are consistent and predictable, even before the wallet is fully deployed. This simplifies wallet creation and management while maintaining compatibility with blockchain standards.
If you're new to the concept of factories, we recommend visiting the What is Factory? page for a more detailed explanation.
Explanation:
This function interacts with the Notus API to either register or query a smart wallet based on the provided Externally Owned Account (EOA).
-
The
salt
is a unique value that, when combined with the EOA and factory, generates a deterministic smart wallet address. If no salt is provided, the default value is zero. Each distinct salt creates a new smart wallet for the same EOA and factory, allowing flexibility while ensuring consistent and predictable address generation.
-
The
POST
request is sent to register a smart wallet if it hasn’t been registered yet. The request uses the EOA, a factory address, and a salt value. -
If the
POST
request fails (e.g., because the wallet is already registered), a fallbackGET
request retrieves the existing smart wallet details. -
Once the smart wallet address is retrieved, it’s stored in the
accountAbstraction
state for further use.
5. Generate a Swap QuoteCopied!
Next, let’s request a swap quote for exchanging tokens:
"use client";
import { polygon } from "viem/chains";
//...//
export default function App() {
const [externallyOwnedAccount, setExternallyOwnedAccount] = useState("");
const [accountAbstraction, setAccountAbstraction] = useState("");
const [quote, setQuote] = useState<SwapQuote | undefined>();
//...//
const getSwapQuote = async () => {
const swapParams = {
payGasFeeToken: "<token-address>",
tokenIn: "<token-address>",
tokenOut: "<token-address>",
amountIn: "5",
walletAddress: accountAbstraction,
toAddress: accountAbstraction,
signerAddress: externallyOwnedAccount,
chainIdIn: polygon.id,
chainIdOut: polygon.id,
gasFeePaymentMethod: "DEDUCT_FROM_AMOUNT",
};
const res = await fetch("https://<baseUrl>/api/v1/crypto/swap", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "<api-key>",
},
body: JSON.stringify(swapParams),
});
if (!res.ok) return;
const data = await res.json();
setQuote(data);
};
return <button onClick={() => getSwapQuote()}>Swap Quote</button>;
}
If you are swapping from a native token (e.g., ETH, BNB, AVAX), use the following address as
tokenIn
:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
. This ensures that the Notus API recognizes the asset as a native token.
Explanation:
-
The
swapParams
object contains details of the swap, including token addresses, wallet address, and fees. -
The Notus API returns a swap quote, which is stored in the
quote
state.
6. Sign and Execute the User OperationCopied!
Finally, let’s sign the swap quote and execute the User Operation:
A UserOperation is a pseudo-transaction used in Account Abstraction (ERC-4337). It defines actions for a smart contract account to execute and is sent to a dedicated mempool, where bundlers group and forward these operations to the EntryPoint contract for validation and execution. Unlike traditional transactions, it offers greater flexibility, such as paying fees with any tokens or executing custom logic.
export default function App() {
const [quote, setQuote] = useState<any>();
const [txHash, setTxHash] = useState("");
const signingAndExecute = async () => {
if (!quote?.swap.quoteId) return;
const quoteId = quote.swap.quoteId;
const signature = await account.signMessage({
account: externallyOwnedAccount,
message: {
raw: quoteId,
},
});
const res = await fetch("https://<baseUrl>/api/v1/crypto/execute-user-op", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "<api-key>",
},
body: JSON.stringify({
quoteId,
signature,
}),
});
if (!res.ok) return;
const data = await res.json();
setTxHash(data.userOpHash);
};
console.log(txHash);
return (
<button onClick={() => signingAndExecute()}>
Signing user operation and execute
</button>
);
}
Explanation:
-
The quote is signed using the wallet’s private key.
-
The signed operation is sent to Notus for execution, and the resulting hash (
userOpHash
) confirms the transaction. -
Signatures are validated on-chain, transactions won't be executed unless the EOA match