From 321c53f057029ae4ff4e39ad107159b18d309dbb Mon Sep 17 00:00:00 2001 From: Rahat Date: Thu, 21 Dec 2023 23:26:13 -0500 Subject: [PATCH] vite tutorial updates --- docs/tutorials/React_vite/gasless-txn.md | 327 +------------------ docs/tutorials/React_vite/initialize.md | 10 +- docs/tutorials/React_vite/sdk-integration.md | 262 ++------------- 3 files changed, 44 insertions(+), 555 deletions(-) diff --git a/docs/tutorials/React_vite/gasless-txn.md b/docs/tutorials/React_vite/gasless-txn.md index edecc7a4..8c7870b7 100644 --- a/docs/tutorials/React_vite/gasless-txn.md +++ b/docs/tutorials/React_vite/gasless-txn.md @@ -16,14 +16,13 @@ In the return for this component lets add the following JSX:

Biconomy Smart Accounts using social login + Gasless Transactions

- {!smartAccount && !loading && } + {!smartAccount && } {loading &&

Loading account details...

} {!!smartAccount && (

Smart account address:

-

{smartAccount.address}

+

address}

-
)}

@@ -39,147 +38,8 @@ In the return for this component lets add the following JSX:

``` -If you followed all instructions from the last step to now your file should look something like this: - -```js -import './App.css' -import "@Biconomy/web3-auth/dist/src/style.css" -import { useState, useEffect, useRef } from 'react' -import SocialLogin from "@biconomy/web3-auth" -import { ChainId } from "@biconomy/core-types"; -import { ethers } from 'ethers' -import { IBundler, Bundler } from '@biconomy/bundler' -import { BiconomySmartAccount,BiconomySmartAccountConfig, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account" -import { IPaymaster, BiconomyPaymaster,} from '@biconomy/paymaster' -import Counter from './Components/Counter'; -import styles from '@/styles/Home.module.css' - - -const bundler: IBundler = new Bundler({ - bundlerUrl: 'https://bundler.biconomy.io/api/v2/80001/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44', // you can get this value from biconomy dashboard. - chainId: ChainId.POLYGON_MUMBAI, - entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, -}) - -const paymaster: IPaymaster = new BiconomyPaymaster({ - paymasterUrl: 'https://paymaster.biconomy.io/api/v1/80001/cIhIeS-I0.7e1f17b1-6ebb-454c-8499-c5f66dd098c6' -}) - -export default function Home() { - const [smartAccount, setSmartAccount] = useState(null) - const [interval, enableInterval] = useState(false) - const sdkRef = useRef(null) - const [loading, setLoading] = useState(false) - const [provider, setProvider] = useState(null); - - useEffect(() => { - let configureLogin:any - if (interval) { - configureLogin = setInterval(() => { - if (!!sdkRef.current?.provider) { - setupSmartAccount() - clearInterval(configureLogin) - } - }, 1000) - } - }, [interval]) - - async function login() { - if (!sdkRef.current) { - const socialLoginSDK = new SocialLogin() - const signature1 = await socialLoginSDK.whitelistUrl("http://127.0.0.1:5173/") - await socialLoginSDK.init({ - chainId: ethers.utils.hexValue(ChainId.POLYGON_MUMBAI).toString(), - network: "testnet", - whitelistUrls: { - "http://127.0.0.1:5173/": signature1, - } - }) - sdkRef.current = socialLoginSDK - } - if (!sdkRef.current.provider) { - sdkRef.current.showWallet() - enableInterval(true) - } else { - setupSmartAccount() - } - } - - async function setupSmartAccount() { - if (!sdkRef?.current?.provider) return - sdkRef.current.hideWallet() - setLoading(true) - const web3Provider = new ethers.providers.Web3Provider( - sdkRef.current.provider - ) - setProvider(web3Provider) - - try { - const biconomySmartAccountConfig: BiconomySmartAccountConfig = { - signer: web3Provider.getSigner(), - chainId: ChainId.POLYGON_MUMBAI, - bundler: bundler, - paymaster: paymaster - } - let biconomySmartAccount = new BiconomySmartAccount(biconomySmartAccountConfig) - biconomySmartAccount = await biconomySmartAccount.init() - console.log("owner: ", biconomySmartAccount.owner) - console.log("address: ", await biconomySmartAccount.getSmartAccountAddress()) - console.log("deployed: ", await biconomySmartAccount.isAccountDeployed( await biconomySmartAccount.getSmartAccountAddress())) - - setSmartAccount(biconomySmartAccount) - setLoading(false) - } catch (err) { - console.log('error setting up smart account... ', err) - } - } - - const logout = async () => { - if (!sdkRef.current) { - console.error('Web3Modal not initialized.') - return - } - await sdkRef.current.logout() - sdkRef.current.hideWallet() - setSmartAccount(null) - enableInterval(false) - } - - return ( -
-

Biconomy Smart Accounts using social login + Gasless Transactions

- - { - !smartAccount && !loading && - } - { - loading &&

Loading account details...

- } - { - !!smartAccount && ( -
-

Smart account address:

-

{smartAccount.address}

- - -
- ) - } -

- Edit src/App.tsx and save to test -

- - Click here to check out the docs - -
- ) -} - - -``` - Now lets create our Counter component! If you do not already have a Components folder go ahead and create one within source and create a new file called Counter.tsx @@ -317,37 +177,29 @@ const tx1 = { data: data, }; -let partialUserOp = await smartAccount.buildUserOp([tx1]); +const userOp = await smartAccount.buildUserOp([transaction], { + paymasterServiceData: { + mode: PaymasterMode.SPONSORED, + }, +}); -const biconomyPaymaster = - smartAccount.paymaster as IHybridPaymaster; -let paymasterServiceData: SponsorUserOperationDto = { - mode: PaymasterMode.SPONSORED, - // optional params... -}; ``` -- Function `incrementCount` of the smart contract is being prepared using the `ethers.utils.Interface `to encode the function data. -- A transaction object tx1 is created with the target contract address **(counterAddress)** and the encoded function data (data), representing the "incrementCount()" function call. -- The smartAccount is used to build a partial user operation `partialUserOp` that includes tx1. The `paymasterServiceData` is prepared with optional parameters, specifying that the operation is sponsored. The IHybridPaymaster type ensures that the `smartAccount.paymaster` supports the sponsored mode for handling payment processing. -- Here, we are supporting gasless transaction, which is why we setup `mode: PaymasterMode.SPONSORED` +Now that the userOp is built and will be sponsored, lets send the final userOp. Now, let's build try and catch block : ```ts try { - const paymasterAndDataResponse = await biconomyPaymaster.getPaymasterAndData(partialUserOp, paymasterServiceData); - partialUserOp.paymasterAndData = paymasterAndDataResponse.paymasterAndData; - const userOpResponse = await smartAccount.sendUserOp(partialUserOp); const transactionDetails = await userOpResponse.wait(); console.log("Transaction Details:", transactionDetails); - console.log("Transaction Hash:", userOpResponse.userOpHash); + console.log("Transaction Hash:", transactionDetails.receipt.transactionHash); - toast.success(`Transaction Hash: ${userOpResponse.userOpHash}`, { + toast.success(`Transaction Hash: ${transactionDetails.receipt.transactionHash}`, { position: "top-right", autoClose: 5000, hideProgressBar: false, @@ -382,10 +234,6 @@ try { Now, let's break down what's happening above : -- **`const paymasterAndDataResponse` = await biconomyPaymaster.getPaymasterAndData(partialUserOp, paymasterServiceData);**: Calls the getPaymasterAndData function on the biconomyPaymaster instance. It sends the partialUserOp and paymasterServiceData as arguments to fetch the necessary information and data related to the sponsored user operation. - -- **`partialUserOp.paymasterAndData` = paymasterAndDataResponse.paymasterAndData;** : The paymasterAndData received from the previous step is added to the partialUserOp object. This likely includes data and configuration needed for the sponsored user operation. - - **`const userOpResponse` = await smartAccount.sendUserOp(partialUserOp);** : The partialUserOp containing the transaction details and paymaster information is sent as a user operation (sendUserOp) to the smartAccount. The smartAccount handles the meta-transaction and submits it to the blockchain. - **`const transactionDetails` = await userOpResponse.wait();** : The wait() function is called on the userOpResponse, which awaits the completion of the user operation transaction. It returns the transaction details once the transaction is confirmed on the blockchain. @@ -434,157 +282,4 @@ export default Counter; ``` -Congratulations you just created your first AA powered dApp. Users can now log in and have a smart account created for them and interact with a smart contract without the need to paying gas fees. Here is the complete implimintation of **`Counter.tsx`**: - -```ts -import React, { useState, useEffect } from "react"; -import { BiconomySmartAccount} from "@biconomy/account" -import { IHybridPaymaster,SponsorUserOperationDto, PaymasterMode,} from '@biconomy/paymaster' -import abi from "../utils/counterAbi.json"; -import { ethers } from "ethers"; -import { ToastContainer, toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; - - -interface Props { - smartAccount: BiconomySmartAccount - provider: any -} - -const TotalCountDisplay: React.FC<{ count: number }> = ({ count }) => { - return
Total count is {count}
; -}; - -const Counter: React.FC = ({ smartAccount, provider }) => { - const [count, setCount] = useState(0); - const [counterContract, setCounterContract] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const counterAddress = import.meta.env.VITE_COUNTER_CONTRACT_ADDRESS; - - useEffect(() => { - setIsLoading(true); - getCount(false); - }, []); - - const getCount = async (isUpdating: boolean) => { - const contract = new ethers.Contract(counterAddress, abi, provider); - setCounterContract(contract); - const currentCount = await contract.count(); - setCount(currentCount.toNumber()); - if (isUpdating) { - toast.success('Count has been updated!', { - position: "top-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "dark", - }); - } - }; - - const incrementCount = async () => { - try { - toast.info('Processing count on the blockchain!', { - position: "top-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "dark", - }); - - const incrementTx = new ethers.utils.Interface(["function incrementCount()"]); - const data = incrementTx.encodeFunctionData("incrementCount"); - - const tx1 = { - to: counterAddress, - data: data, - }; - - let partialUserOp = await smartAccount.buildUserOp([tx1]); - - const biconomyPaymaster = smartAccount.paymaster as IHybridPaymaster; - - let paymasterServiceData: SponsorUserOperationDto = { - mode: PaymasterMode.SPONSORED, - smartAccountInfo: { - name: 'BICONOMY', - version: '2.0.0' - }, - // optional params... - }; - - try { - const paymasterAndDataResponse = await biconomyPaymaster.getPaymasterAndData(partialUserOp, paymasterServiceData); - partialUserOp.paymasterAndData = paymasterAndDataResponse.paymasterAndData; - - const userOpResponse = await smartAccount.sendUserOp(partialUserOp); - const transactionDetails = await userOpResponse.wait(); - - console.log("Transaction Details:", transactionDetails); - console.log("Transaction Hash:", userOpResponse.userOpHash); - - toast.success(`Transaction Hash: ${userOpResponse.userOpHash}`, { - position: "top-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "dark", - }); - - getCount(true); - } catch (e) { - console.error("Error executing transaction:", e); - // ... handle the error if needed ... - } - } catch (error) { - console.error("Error executing transaction:", error); - toast.error('Error occurred, check the console', { - position: "top-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "dark", - }); - } - }; - - return ( - <> - - -

- - - ); -}; - -export default Counter; - -``` - -If you would like to see the completed project on github you can use the template below: -https://github.com/vanshika-srivastava/scw-gasless-bico-modular +Congratulations you just created your first AA powered dApp. Users can now log in and have a smart account created for them and interact with a smart contract without the need to paying gas fees. \ No newline at end of file diff --git a/docs/tutorials/React_vite/initialize.md b/docs/tutorials/React_vite/initialize.md index 74a6ff71..ba8de722 100644 --- a/docs/tutorials/React_vite/initialize.md +++ b/docs/tutorials/React_vite/initialize.md @@ -37,15 +37,7 @@ things simple we will be using Yarn from this point. Install the following dependencies: ```bash -yarn add - @biconomy/account - @biconomy/bundler - @biconomy/common - @biconomy/core-types - @biconomy/paymaster - @biconomy/web3-auth - @biconomy/modules - ethers@5.7.2 +yarn add @biconomy/account @biconomy/bundler @biconomy/common @biconomy/core-types @biconomy/paymaster magic-sdk @biconomy/modules ethers@5.7.2 ``` We will use these tools to build out our front end. In addition, let's also diff --git a/docs/tutorials/React_vite/sdk-integration.md b/docs/tutorials/React_vite/sdk-integration.md index f58d5f1f..9a1c6ddd 100644 --- a/docs/tutorials/React_vite/sdk-integration.md +++ b/docs/tutorials/React_vite/sdk-integration.md @@ -11,9 +11,7 @@ following: ```js import "./App.css"; -import "@biconomy/web3-auth/dist/src/style.css"; import { useState, useEffect, useRef } from "react"; -import SocialLogin from "@biconomy/web3-auth"; import { ChainId } from "@biconomy/core-types"; import { ethers } from "ethers"; import { IBundler, Bundler } from "@biconomy/bundler"; @@ -27,6 +25,7 @@ import { DEFAULT_ECDSA_OWNERSHIP_MODULE, } from "@biconomy/modules"; import Counter from "./Components/Counter"; +import { Magic } from "magic-sdk"; ``` We are importing some css styles here but you can build your own login UI as @@ -36,8 +35,8 @@ Here is information about the rest of the imports: - `useState`, `useEffect`, `useRef`: React hooks for managing component state and lifecycle. -- `SocialLogin` from `@biconomy/web3-auth`: A class from Biconomy SDK that - allows you to leverage Web3Auth for social logins. +- `Magic` from `magic-sdk`: A class from Magic SDK that + allows you to leverage Magic for social logins. - ChainId from `@biconomy/core-types`: An enumeration of supported blockchain networks. - `ethers`: A library for interacting with Ethereum. @@ -68,254 +67,57 @@ const paymaster: IPaymaster = new BiconomyPaymaster({ }); ``` -:::info You can get your Paymaster URL and bundler URL from Biconomy Dashboard. +:::info + +You can get your Paymaster URL and bundler URL from Biconomy Dashboard. Follow the steps mentioned -[here](https://docs.biconomy.io/docs/category/biconomy-dashboard). ::: +[here](https://docs.biconomy.io/docs/category/biconomy-dashboard). + +::: Let's take a look at some state variables that will help us with our implementation: ```js -const [smartAccount, setSmartAccount] = useState < any > null; -const [interval, enableInterval] = useState(false); -const sdkRef = (useRef < SocialLogin) | (null > null); -const [loading, setLoading] = useState < boolean > false; -const [provider, setProvider] = useState < any > null; +const [smartAccount, setSmartAccount] = useState < any > (null); +const [loading, setLoading] = useState < boolean > (false); +const [provider, setProvider] = useState < any > (null); +const [address, setAddress] = useState ("";) ``` -Here we have some state that will be used to track our smart account that will -be generated with the sdk, an interval that will help us with checking for login -status, a loading state, provider state to track our web3 provider and a -reference to our Social login sdk. +Next let's implement the connect function for activating magic social login and Biconomy Smart Account creation: -Next let's add a `useEffect` hook: ```js -useEffect(() => { - let configureLogin: any; - if (interval) { - configureLogin = setInterval(() => { - if (!!sdkRef.current?.provider) { - setupSmartAccount(); - clearInterval(configureLogin); - } - }, 1000); - } -}, [interval]); -``` -This use effect will be triggered after we open our login component, which we'll -create a function for shortly. Once a user opens the component it will check if -a provider is available and run the functions for setting up the smart account. - -Now let's build our login function: - -```js -async function login() { - if (!sdkRef.current) { - const socialLoginSDK = new SocialLogin(); - const signature1 = await socialLoginSDK.whitelistUrl( - "http://127.0.0.1:5173/", +const connect = async () => { + try { + await magic.wallet.connectWithUI(); + const web3Provider = new ethers.providers.Web3Provider( + magic.rpcProvider, + "any", ); - await socialLoginSDK.init({ - chainId: ethers.utils.hexValue(ChainId.POLYGON_MUMBAI).toString(), - network: "testnet", - whitelistUrls: { - "http://127.0.0.1:5173/": signature1, - }, + setProvider(web3Provider) + const module = await ECDSAOwnershipValidationModule.create({ + signer: web3Provider.getSigner(), + moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE, }); - sdkRef.current = socialLoginSDK; - } - if (!sdkRef.current.provider) { - sdkRef.current.showWallet(); - enableInterval(true); - } else { - setupSmartAccount(); - } -} -``` - -The `login` function is an asynchronous function that handles the login flow for -the application. Here's a step-by-step explanation: - -1. **SDK Initialization**: The function first checks if the `sdkRef` object - (which is a reference to the Biconomy SDK instance) is null. If it is, it - means that the SDK is not yet initialized. In this case, it creates a new - instance of `SocialLogin` (a Biconomy SDK component), whitelists a local URL - (`http://127.0.0.1:5173/`), and initializes the SDK with the Polygon Mumbai - testnet configuration and the whitelisted URL. After initialization, it - assigns the SDK instance to sdkRef.current. -2. **Provider Check**: After ensuring the SDK is initialized, the function - checks if the provider of the `sdkRef` object is set. If it is not, it means - the user is not yet logged in. It then shows the wallet interface for the - user to login using `sdkRef.current.showWallet()`, and enables the interval - by calling `enableInterval(true)`. This interval (setup in a useEffect hook - elsewhere in the code) periodically checks if the provider is available and - sets up the smart account once it is. -3. **Smart Account Setup**: If the provider of sdkRef is already set, it means - the user is logged in. In this case, it directly sets up the smart account by - calling `setupSmartAccount()`. - -In summary, the `login` function handles the SDK initialization and login flow. -It initializes the SDK if it's not already initialized, shows the wallet -interface for the user to login if they're not logged in, and sets up the smart -account if the user is logged in. - -::: - -:::caution - -It is important to make sure that you update the whitelist URL with your -production url when you are ready to go live! - -::: - -Now lets actually set up the smart account: - -```js -async function setupSmartAccount() { - if (!sdkRef?.current?.provider) return; - sdkRef.current.hideWallet(); - setLoading(true); - const web3Provider = new ethers.providers.Web3Provider( - sdkRef.current.provider, - ); - setProvider(web3Provider); - - const module = await ECDSAOwnershipValidationModule.create({ - signer: web3Provider.getSigner(), - moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE, - }); - - try { + setLoading(true) let biconomySmartAccount = await BiconomySmartAccountV2.create({ chainId: ChainId.POLYGON_MUMBAI, bundler: bundler, + paymaster: paymaster, entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, defaultValidationModule: module, activeValidationModule: module, }); - console.log("address: ", await biconomySmartAccount.getAccountAddress()); - console.log( - "deployed: ", - await biconomySmartAccount.isAccountDeployed(smartAccount.accountAddress), - ); - setSmartAccount(biconomySmartAccount); - setLoading(false); - } catch (err) { - console.log("error setting up smart account... ", err); + const address = await biconomySmartAccount.getAccountAddress(); + setAddress(address) + setLoading(false) + } catch (error) { + console.error(error); } -} -``` - -The `setupSmartAccount` function is an asynchronous function used to initialize -a smart account with Biconomy and connect it with the Web3 provider. Here's a -step-by-step explanation of what it does: - -1. **if `(!sdkRef?.current?.provider) return:`** Checks if the sdkRef object - exists, and if it does, whether it has a provider property. If either of - these conditions is not met, the function returns early and does not proceed - further. - -2. **`sdkRef.current.hideWallet():`** Line calls the hideWallet() method on the - sdkRef.current object. It appears to be a method provided by the sdkRef - object, and it is likely used to hide the wallet or authentication interface - for the user. - -3. **`setLoading(true):`** Sets the state variable loading to true. It seems - like loading is used to indicate that some asynchronous operation is in - progress, and the UI might display a loading indicator during this time. - -4. **`const web3Provider `= new - ethers.providers.Web3Provider(sdkRef.current.provider):** Creates a new - Web3Provider instance using the sdkRef.current.provider as the Web3 provider. - It assumes that sdkRef.current.provider is a valid Web3 provider, possibly - obtained from Biconomy's SDK. - -5. **`setProvider(web3Provider):`** Sets the web3Provider created in the - previous step as the state variable provider. This step likely enables other - parts of the application to access the Web3 provider. - -**Setting up BiconomySmartAccount:** - -6. **`BiconomySmartAccountV2.create()`** - Creates an instance of the BiconomySmartAccount. The configuration includes the following properties: - -- `signer:` The signer (wallet) associated with the web3Provider. -- `chainId:` The chain ID, which is set to ChainId.POLYGON_MUMBAI. This - specifies the blockchain network where the BiconomySmartAccount is being - used (Polygon Mumbai, in this case). -- `bundler:` The bundler used for optimizing and bundling smart contracts. It - is expected that the bundler variable is defined elsewhere in the code. -- `paymaster:` The paymaster used for handling payment processing. It is - expected that the paymaster variable is defined elsewhere in the code. - -7. Logging `BiconomySmartAccount` information: - -- **console.log("owner: ", biconomySmartAccount.owner):** Logs the owner of - the BiconomySmartAccount. The owner property might represent the Ethereum - address of the smart account owner. - -- **console.log("address: ", await - biconomySmartAccount.getAccountAddress()):** Logs the Ethereum address - of the BiconomySmartAccount using the getAccountAddress() method. This - address is the entrypoint address mentioned earlier, and it serves as the - point of entry for interacting with the smart account through Biconomy. - -- **`console.log("deployed: ", await biconomySmartAccount.isAccountDeployed(await biconomySmartAccount.getAccountAddress()))`:** - Logs whether the smart account has been deployed or not. It calls the - isAccountDeployed() method on the BiconomySmartAccount instance, passing the - entrypoint address as an argument. - -8. **`setSmartAccount(biconomySmartAccount)`:** Sets the biconomySmartAccount as - the state variable smartAccount. This step makes the BiconomySmartAccount - instance available to other parts of the application. - -9. **`setLoading(false):`** Sets the state variable loading to false, indicating - that the asynchronous operation is complete. - -10. **Error handling:** If any errors occur during the execution of the - function, the catch block will catch the error, and it will be logged to the - console. - -So, in summary, the `setupSmartAccount` function checks the availability of the -Biconomy provider, hides the wallet interface, sets up a Web3 provider, creates -and initializes a smart account, and then saves this account and the Web3 -provider in the state. If any error occurs during this process, it is logged to -the console. - -Finally our last function will be a logout function: - -```js -const logout = async () => { - if (!sdkRef.current) { - console.error("Web3Modal not initialized."); - return; - } - await sdkRef.current.logout(); - sdkRef.current.hideWallet(); - setSmartAccount(null); - enableInterval(false); }; -``` -The `logout` function is an asynchronous function that handles the logout flow -for the application. Here's a breakdown of its functionality: - -1. **Check SDK Initialization**: The function first checks if the `sdkRef` - object (which is a reference to the Biconomy SDK instance) is null. If it is, - it means that the SDK is not yet initialized. In this case, it logs an error - message and returns immediately without executing the rest of the function. -2. **Logout and Hide Wallet**: If the SDK is initialized, it logs the user out - by calling `sdkRef.current.logout()`. This is an asynchronous operation, - hence the await keyword. It then hides the wallet interface by calling - `sdkRef.current.hideWallet()`. -3. **Clear Smart Account and Interval**: After logging the user out and hiding - the wallet, it clears the smart account by calling `setSmartAccount(null)`, - and disables the interval by calling `enableInterval(false)`. - -In summary, the logout function checks if the SDK is initialized, logs the user -out and hides the wallet if it is, and then clears the smart account and -disables the interval. If the SDK is not initialized, it logs an error message -and does not execute the rest of the function. +```