+ {" "}
+ Notes:
+ 1. If you are using metamask and transactions hang. Go to settings > advanced > clear activity and nonce data
+ 2. First ZK Proof generation takes longer time, especially with bad internet as there is a
+ zkey file to download once in the data vault connection
+ 3. The more proofs you request, the longer it takes to generate them (about 2 secs per
+ proof)
+
>
);
diff --git a/front/src/app/globals.css b/front/src/app/globals.css
index 9707953..66f1a08 100644
--- a/front/src/app/globals.css
+++ b/front/src/app/globals.css
@@ -158,11 +158,12 @@ input:focus {
.callout {
- color: #d3d3d3; /* Light gray color */
+ color: #000000; /* Black color text */
padding: 1rem; /* Adds some space around the text */
- border: 1px solid #d3d3d3; /* Adds a light gray border */
+ border: 2px solid #000000; /* Increases border thickness and change to black */
border-radius: 5px; /* Rounds the corners of the border */
- background-color: rgba(0,0,0,0.1); /* Adds a very light black background to increase readability of gray text */
+ background-color: rgba(255,255,255,0.6); /* Adds a semi-transparent white background to increase readability of black text */
+ box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); /* Adds a slight shadow for a "lifted" effect */
}
.verifying, .verified {
diff --git a/front/src/app/page.tsx b/front/src/app/page.tsx
index 25e9449..b6806ee 100644
--- a/front/src/app/page.tsx
+++ b/front/src/app/page.tsx
@@ -1,126 +1,50 @@
"use client";
-import { useEffect, useState } from "react";
+import { useState } from "react";
import Header from "./components/Header";
-import {
- useAccount,
- useConnect,
- useContractWrite,
- useDisconnect,
- useNetwork,
- usePrepareContractWrite,
- useSwitchNetwork,
-} from "wagmi";
-import { waitForTransaction, readContract } from "@wagmi/core";
+import { useAccount, useNetwork, useSwitchNetwork } from "wagmi";
import { useConnectModal } from "@rainbow-me/rainbowkit";
-import { decodeEventLog, formatEther } from "viem";
-import { abi as AirdropABI } from "../../../abi/Airdrop.json";
-import { formatError, signMessage } from "@/utils/misc";
+import {
+ getProofDataForAuth,
+ getProofDataForClaim,
+ getuserIdFromHex,
+ signMessage,
+} from "@/utils/misc";
import { mumbaiFork } from "@/utils/wagmi";
import {
SismoConnectButton, // the Sismo Connect React button displayed below
} from "@sismo-core/sismo-connect-react";
-import { transactions } from "../../../broadcast/Airdrop.s.sol/5151111/run-latest.json";
import { fundMyAccountOnLocalFork } from "@/utils/fundMyAccountOnLocalFork";
-import { errorsABI } from "@/utils/errorsABI";
-import {
- AUTHS,
- CLAIMS,
- CONFIG,
- VerifiedAuth,
- VerifiedClaim,
- AuthType,
- ClaimType,
-} from "@/app/sismo-connect-config";
+import { AUTHS, CLAIMS, CONFIG, AuthType, ClaimType } from "@/app/sismo-connect-config";
+import useContract from "@/utils/useContract";
/* ******************** Defines the chain to use *************************** */
const CHAIN = mumbaiFork;
export default function Home() {
- /* *********************** Application states *************************** */
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState("");
- const [pageState, setPageState] = useState("init");
- const [amountClaimed, setAmountClaimed] = useState("");
- const [responseBytes, setResponseBytes] = useState("");
- const [verifiedClaims, setVerifiedClaims] = useState();
- const [verifiedAuths, setVerifiedAuths] = useState();
- const [verifiedSignedMessage, setVerifiedSignedMessage] = useState();
-
- /* *************** Wagmi hooks for wallet connection ******************** */
- const { disconnect } = useDisconnect();
- const { chain } = useNetwork();
+ const [responseBytes, setResponseBytes] = useState(null);
const { isConnected, address } = useAccount({
onConnect: async ({ address }) => address && (await fundMyAccountOnLocalFork(address)),
});
- const { switchNetworkAsync, switchNetwork } = useSwitchNetwork();
+ const { chain } = useNetwork();
+ const { switchNetwork } = useSwitchNetwork();
const { openConnectModal, connectModalOpen } = useConnectModal();
- /* ************* Wagmi hooks for contract interaction ******************* */
- const contractCallInputs =
- responseBytes && chain
- ? {
- address: transactions[0].contractAddress as `0x${string}}`,
- abi: [...AirdropABI, ...errorsABI],
- functionName: "claimWithSismo",
- args: [responseBytes],
- chain,
- }
- : {};
-
- const { config, error: wagmiSimulateError } = usePrepareContractWrite(contractCallInputs);
- const { writeAsync } = useContractWrite(config);
-
- /* ************* Handle simulateContract call & chain errors ************ */
- useEffect(() => {
- if (chain?.id !== CHAIN.id) return setError(`Please switch to ${CHAIN.name} network`);
- setError("");
- }, [chain]);
-
- useEffect(() => {
- if (!wagmiSimulateError) return;
- if (!isConnected) return;
- return setError(formatError(wagmiSimulateError));
- }, [wagmiSimulateError, isConnected]);
-
- /* ************ Handle the airdrop claim button click ******************* */
- async function claimAirdrop() {
- if (!address) return;
- setError("");
- setLoading(true);
- try {
- // Switch to the selected network if not already on it
- if (chain?.id !== CHAIN.id) await switchNetworkAsync?.(CHAIN.id);
- setPageState("confirmingTransaction");
- const tx = await writeAsync?.();
- setPageState("verifying");
- const txReceipt = tx && (await waitForTransaction({ hash: tx.hash }));
- if (txReceipt?.status === "success") {
- setAmountClaimed(
- formatEther((await readAirdropContract("balanceOf", [address])) as unknown as bigint)
- );
- setVerifiedClaims((await readAirdropContract("getVerifiedClaims")) as VerifiedClaim[]);
- setVerifiedAuths((await readAirdropContract("getVerifiedAuths")) as VerifiedAuth[]);
- setVerifiedSignedMessage((await readAirdropContract("getVerifiedSignedMessage")) as string);
- setPageState("verified");
- }
- } catch (e: any) {
- setError(formatError(e));
- } finally {
- setLoading(false);
- }
- }
+ const {
+ claimAirdrop,
+ reset,
+ amountClaimed,
+ error,
+ pageState,
+ verifiedAuths,
+ verifiedClaims,
+ verifiedSignedMessage,
+ } = useContract({ responseBytes, chain: CHAIN });
/* ************************* Reset state **************************** */
function resetApp() {
- disconnect();
- setAmountClaimed("");
+ reset();
setResponseBytes("");
- setError("");
- setPageState("init");
- const url = new URL(window.location.href);
- url.searchParams.delete("sismoConnectResponseCompressed");
- window.history.replaceState({}, "", url.toString());
}
return (
@@ -136,7 +60,7 @@ export default function Home() {
<>
<>
{" "}
-
+ {pageState != "init" && }
{`Chain: ${chain?.name} [${chain?.id}]`}
@@ -147,31 +71,22 @@ export default function Home() {
<>
{
setResponseBytes(responseBytes);
- setPageState("responseReceived");
}}
// Some text to display on the button
text={"Claim with Sismo"}
/>
-
- {" "}
- Notes:
- 1. First ZK Proof generation takes longer time, especially with bad internat as
- there is a zkey file to download once in the data vault connection
- 2. The more proofs you request, the longer it takes to generate them (about 2 secs
- per proof)
-
>
)}
@@ -188,7 +103,7 @@ export default function Home() {
{isConnected && !amountClaimed && error && (
<>
-
{error}
+
{error}
{error.slice(0, 16) === "Please switch to" && (
)}
@@ -353,53 +268,3 @@ export default function Home() {
>
);
}
-
-function readibleHex(userId: string, startLength = 6, endLength = 4, separator = "...") {
- if (!userId.toString().startsWith("0x")) {
- return userId; // Return the original string if it doesn't start with "0x"
- }
- return userId.substring(0, startLength) + separator + userId.substring(userId.length - endLength);
-}
-
-function getProofDataForAuth(verifiedAuths: VerifiedAuth[], authType: AuthType): string | null {
- for (const auth of verifiedAuths) {
- if (auth.proofData && auth.authType === authType) {
- return readibleHex("0x" + (auth.proofData as unknown as number).toString(16));
- }
- }
-
- return null; // returns null if no matching authType is found
-}
-
-function getProofDataForClaim(
- verifiedClaims: VerifiedClaim[],
- claimType: number,
- groupId: string,
- value: number
-): string | null {
- for (const claim of verifiedClaims) {
- if (claim.proofData && claim.claimType === claimType && claim.groupId === groupId) {
- return readibleHex("0x" + (claim.proofData as unknown as number).toString(16));
- }
- }
-
- return null; // returns null if no matching authType is found
-}
-
-function getuserIdFromHex(hexUserId: string) {
- const index = hexUserId.lastIndexOf("000000");
- if (index !== -1) {
- return hexUserId.substring(index + 6);
- } else {
- return hexUserId; // returns the original string if '00' is not found
- }
-}
-
-const readAirdropContract = async (functionName: string, args?: string[]) => {
- return readContract({
- address: transactions[0].contractAddress as `0x${string}}`,
- abi: AirdropABI,
- functionName,
- args: args || [],
- });
-};
diff --git a/front/src/app/sismo-connect-config.ts b/front/src/app/sismo-connect-config.ts
index a161c68..1e5afa8 100644
--- a/front/src/app/sismo-connect-config.ts
+++ b/front/src/app/sismo-connect-config.ts
@@ -7,8 +7,10 @@ import {
VerifiedAuth,
VerifiedClaim,
} from "@sismo-core/sismo-connect-client";
+import { SignatureRequest } from "@sismo-core/sismo-connect-react";
-export { ClaimType, AuthType, VerifiedAuth, VerifiedClaim };
+export { ClaimType, AuthType };
+export type { VerifiedAuth, VerifiedClaim };
export const CONFIG: SismoConnectConfig = {
appId: "0x32403ced4b65f2079eda77c84e7d2be6",
vault: {
@@ -33,6 +35,7 @@ export const CONFIG: SismoConnectConfig = {
// Sismo Connect Response in the vault instead of redirecting back to the app
};
+// Request users to prove ownership of a Data Source (Wallet, Twitter, Github, Telegram, etc.)
export const AUTHS: AuthRequest[] = [
// Anonymous identifier of the vault for this app
// vaultId = hash(vaultSecret, appId).
@@ -44,6 +47,7 @@ export const AUTHS: AuthRequest[] = [
// { authType: AuthType.TELEGRAM, userId: "875608110", isOptional: true },
];
+// Request users to prove membership in a Data Group (e.g I own a wallet that is part of a DAO, owns an NFT, etc.)
export const CLAIMS: ClaimRequest[] = [
{
// claim on Sismo Hub GitHub Contributors Data Group membership: https://factory.sismo.io/groups-explorer?search=0xda1c3726426d5639f4c6352c2c976b87
@@ -69,11 +73,12 @@ export const CLAIMS: ClaimRequest[] = [
// request user to prove membership in the group with value = 10
groupId: "0xfae674b6cba3ff2f8ce2114defb200b1",
claimType: ClaimType.EQ,
- value: 10, // dhadrin.sismo.eth minted exactly 10, eligible
+ value: 10, // dhadrien.sismo.eth minted exactly 10, eligible
isOptional: true,
},
];
+// Request users to sign a message
export const SIGNATURE_REQUEST: SignatureRequest = {
message: "I love Sismo!",
isSelectableByUser: true,
diff --git a/front/src/utils/misc.ts b/front/src/utils/misc.ts
index cd08f66..ffe7a9a 100644
--- a/front/src/utils/misc.ts
+++ b/front/src/utils/misc.ts
@@ -1,6 +1,7 @@
import { encodeAbiParameters } from "viem";
import { abi as AirdropABI } from "../../../abi/Airdrop.json";
import { errorsABI } from "./errorsABI";
+import { AuthType, VerifiedAuth, VerifiedClaim } from "@/app/sismo-connect-config";
declare global {
interface Window {
@@ -29,3 +30,44 @@ export const formatError = (error: Error | null) => {
if (!error) return "";
return error?.message?.split("args:")?.[0]?.split("data:")?.[0]?.trim() || "";
};
+
+export function readibleHex(userId: string, startLength = 6, endLength = 4, separator = "...") {
+ if (!userId.toString().startsWith("0x")) {
+ return userId; // Return the original string if it doesn't start with "0x"
+ }
+ return userId.substring(0, startLength) + separator + userId.substring(userId.length - endLength);
+}
+
+export function getProofDataForAuth(verifiedAuths: VerifiedAuth[], authType: AuthType): string | null {
+ for (const auth of verifiedAuths) {
+ if (auth.proofData && auth.authType === authType) {
+ return readibleHex("0x" + (auth.proofData as unknown as number).toString(16));
+ }
+ }
+
+ return null; // returns null if no matching authType is found
+}
+
+export function getProofDataForClaim(
+ verifiedClaims: VerifiedClaim[],
+ claimType: number,
+ groupId: string,
+ value: number
+): string | null {
+ for (const claim of verifiedClaims) {
+ if (claim.proofData && claim.claimType === claimType && claim.groupId === groupId) {
+ return readibleHex("0x" + (claim.proofData as unknown as number).toString(16));
+ }
+ }
+
+ return null; // returns null if no matching authType is found
+}
+
+export function getuserIdFromHex(hexUserId: string) {
+ const index = hexUserId.lastIndexOf("000000");
+ if (index !== -1) {
+ return hexUserId.substring(index + 6);
+ } else {
+ return hexUserId; // returns the original string if '00' is not found
+ }
+}
\ No newline at end of file
diff --git a/front/src/utils/useContract.tsx b/front/src/utils/useContract.tsx
new file mode 100644
index 0000000..bb6777f
--- /dev/null
+++ b/front/src/utils/useContract.tsx
@@ -0,0 +1,144 @@
+import { useEffect, useState } from "react";
+import { Chain, TransactionReceipt, formatEther } from "viem";
+import {
+ useAccount,
+ useContractWrite,
+ useNetwork,
+ usePrepareContractWrite,
+ useSwitchNetwork,
+} from "wagmi";
+import { waitForTransaction, readContract } from "@wagmi/core";
+import { abi as AirdropABI } from "../../../abi/Airdrop.json";
+import { errorsABI } from "./errorsABI";
+import { formatError } from "./misc";
+import { VerifiedAuth, VerifiedClaim } from "@/app/sismo-connect-config";
+import { fundMyAccountOnLocalFork } from "./fundMyAccountOnLocalFork";
+import { transactions } from "../../../broadcast/Airdrop.s.sol/5151111/run-latest.json";
+
+export type ContractClaim = {
+ claimAirdrop: () => Promise;
+ reset: () => void;
+ error: string;
+ amountClaimed: string;
+ pageState: string;
+ verifiedClaims: VerifiedClaim[] | undefined;
+ verifiedAuths: VerifiedAuth[] | undefined;
+ verifiedSignedMessage: string | undefined;
+};
+
+export default function useContract({
+ responseBytes,
+ chain,
+}: {
+ responseBytes: string | null;
+ chain: Chain;
+}): ContractClaim {
+ const [error, setError] = useState("");
+ const [pageState, setPageState] = useState("init");
+ const [amountClaimed, setAmountClaimed] = useState("");
+ const [verifiedClaims, setVerifiedClaims] = useState();
+ const [verifiedAuths, setVerifiedAuths] = useState();
+ const [verifiedSignedMessage, setVerifiedSignedMessage] = useState();
+ const { chain: currentChain } = useNetwork();
+ const { isConnected, address } = useAccount({
+ onConnect: async ({ address }) => address && (await fundMyAccountOnLocalFork(address)),
+ });
+ const { switchNetworkAsync } = useSwitchNetwork();
+ const contractCallInputs = {
+ address: transactions[0].contractAddress as `0x${string}`,
+ abi: [...AirdropABI, ...errorsABI],
+ functionName: "claimWithSismo",
+ args: [responseBytes],
+ chain,
+ enabled: Boolean(responseBytes),
+ };
+ const { config, error: wagmiSimulateError } = usePrepareContractWrite(contractCallInputs);
+ const { writeAsync } = useContractWrite(config);
+
+ useEffect(() => {
+ if (!responseBytes) return;
+ setPageState("responseReceived");
+ }, [responseBytes]);
+
+ /* ************* Handle simulateContract call & chain errors ************ */
+ useEffect(() => {
+ if (currentChain?.id !== chain.id) return setError(`Please switch to ${chain.name} network`);
+ setError("");
+ }, [currentChain]);
+
+ useEffect(() => {
+ if (!wagmiSimulateError) return;
+ if (!isConnected) return;
+ return setError(formatError(wagmiSimulateError));
+ }, [wagmiSimulateError, isConnected]);
+
+ /* ************ Handle the airdrop claim button click ******************* */
+ async function claimAirdrop() {
+ if (!address) return;
+ setError("");
+ try {
+ if (currentChain?.id !== chain.id) await switchNetworkAsync?.(chain.id);
+ setPageState("confirmingTransaction");
+ const tx = await writeAsync?.();
+ setPageState("verifying");
+ let txReceipt: TransactionReceipt | undefined;
+ if (chain.id === 5151111) {
+ const timeout = new Promise((_, reject) =>
+ setTimeout(() => {
+ setPageState("responseReceived");
+ reject(
+ new Error(
+ "Transaction timed-out: If you are running a local fork on Anvil please make sure to reset your wallet nonce. In metamask: Go to settings > advanced > clear activity and nonce data"
+ )
+ );
+ }, 10000)
+ );
+ const txReceiptPromise = tx && waitForTransaction({ hash: tx.hash });
+ const race = await Promise.race([txReceiptPromise, timeout]);
+ txReceipt = race as TransactionReceipt;
+ } else {
+ txReceipt = tx && (await waitForTransaction({ hash: tx.hash }));
+ }
+ if (txReceipt?.status === "success") {
+ setAmountClaimed(
+ formatEther((await readAirdropContract("balanceOf", [address])) as unknown as bigint)
+ );
+ setVerifiedClaims((await readAirdropContract("getVerifiedClaims")) as VerifiedClaim[]);
+ setVerifiedAuths((await readAirdropContract("getVerifiedAuths")) as VerifiedAuth[]);
+ setVerifiedSignedMessage((await readAirdropContract("getVerifiedSignedMessage")) as string);
+ setPageState("verified");
+ }
+ } catch (e: any) {
+ setError(formatError(e));
+ }
+ }
+
+ const readAirdropContract = async (functionName: string, args?: string[]) => {
+ return readContract({
+ address: transactions[0].contractAddress as `0x${string}}`,
+ abi: AirdropABI,
+ functionName,
+ args: args || [],
+ });
+ };
+
+ function reset() {
+ setAmountClaimed("");
+ setError("");
+ setPageState("init");
+ const url = new URL(window.location.href);
+ url.searchParams.delete("sismoConnectResponseCompressed");
+ window.history.replaceState({}, "", url.toString());
+ }
+
+ return {
+ claimAirdrop,
+ reset,
+ error,
+ pageState,
+ amountClaimed,
+ verifiedClaims,
+ verifiedAuths,
+ verifiedSignedMessage,
+ };
+}
diff --git a/src/Airdrop.sol b/src/Airdrop.sol
index 6711e5d..2cd593d 100644
--- a/src/Airdrop.sol
+++ b/src/Airdrop.sol
@@ -3,23 +3,30 @@ pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "forge-std/console.sol";
-import "sismo-connect-solidity/SismoLib.sol"; // <--- add a Sismo Connect import
+import "sismo-connect-solidity/SismoLib.sol";
/*
* @title Airdrop
* @author Sismo
- * @dev Simple Airdrop contract that mints ERC20 tokens to the msg.sender
- * This contract is used for tutorial purposes only
- * It will be used to demonstrate how to integrate Sismo Connect
+ * @dev Simple Airdrop contract gated by Sismo Connect
+ * Application requests multiple zk proofs (auths and claims) and verify them
+ * The contract stores all verified results in storage
*/
contract Airdrop is ERC20, SismoConnect {
error AlreadyClaimed();
+ event AuthVerified(VerifiedAuth verifiedAuth);
+ event ClaimVerified(VerifiedClaim verifiedClaim);
+ event SignedMessageVerified(bytes verifiedSignedMessage);
using SismoConnectHelper for SismoConnectVerifiedResult;
mapping(uint256 => bool) public claimed;
+ // must correspond to requests defined in the app frontend
+ // Sismo Connect response's zk proofs will be checked against these requests.
+ // check Airdrop.s.sol to see how these requests are built and passed to the constructor
AuthRequest[] private _authRequests;
ClaimRequest[] private _claimRequests;
+ // Results of the verification of the Sismo Connect response.
VerifiedAuth[] internal _verifiedAuths;
VerifiedClaim[] internal _verifiedClaims;
bytes internal _verifiedSignedMessage;
@@ -36,66 +43,48 @@ contract Airdrop is ERC20, SismoConnect {
_setClaims(claimRequests);
}
-// struct SismoConnectVerifiedResult {
-// bytes16 appId;
-// bytes16 namespace;
-// bytes32 version;
-// VerifiedAuth[] auths;
-// VerifiedClaim[] claims;
-// bytes signedMessage;
-// }
-
function claimWithSismo(bytes memory response) public {
SismoConnectVerifiedResult memory result = verify({
responseBytes: response,
- // we want the user to prove that he owns a Sismo Vault
- // we are recreating the auth request made in the frontend to be sure that
- // the proofs provided in the response are valid with respect to this auth request
+ // checking response against requested auths
auths: _authRequests,
+ // checking response against requested claims
claims: _claimRequests,
- // we also want to check if the signed message provided in the response is the signature of the user's address
+ // checking response against requested message signature
signature: buildSignature({message: abi.encode(msg.sender)})
});
- for (uint256 i = 0; i < result.auths.length; i++) {
- _verifiedAuths.push(result.auths[i]);
- }
- for (uint256 i = 0; i < result.claims.length; i++) {
- _verifiedClaims.push(result.claims[i]);
- }
-
- _verifiedSignedMessage =result.signedMessage;
-
- // if the proofs and signed message are valid, we take the userId from the verified result
- // in this case the userId is the vaultId (since we used AuthType.VAULT in the auth request),
// it is the anonymous identifier of a user's vault for a specific app
// --> vaultId = hash(userVaultSecret, appId)
+ // used to avoid double claims
uint256 vaultId = result.getUserId(AuthType.VAULT);
- // we check if the user has already claimed the airdrop
- // if (claimed[vaultId]) {
- // revert AlreadyClaimed();
- // }
+ // checking if the user has already claimed
+ if (claimed[vaultId]) {
+ revert AlreadyClaimed();
+ }
- // we mark the user as claimed. We could also have stored more user airdrop information for a more complex airdrop system. But we keep it simple here.
+ // marking that the user has claimed
claimed[vaultId] = true;
+ // airdrop amount = number of verified proofs
uint256 airdropAmount = (result.auths.length + result.claims.length) * 10 ** 18;
_mint(msg.sender, airdropAmount);
- }
- function _setAuths(AuthRequest[] memory auths) private {
- for (uint256 i = 0; i < auths.length; i++) {
- _authRequests.push(auths[i]);
+ // storing the result of the verification
+ for (uint256 i = 0; i < result.auths.length; i++) {
+ _verifiedAuths.push(result.auths[i]);
+ emit AuthVerified(result.auths[i]);
}
- }
-
- function _setClaims(ClaimRequest[] memory claims) private {
- for (uint256 i = 0; i < claims.length; i++) {
- _claimRequests.push(claims[i]);
+ for (uint256 i = 0; i < result.claims.length; i++) {
+ _verifiedClaims.push(result.claims[i]);
+ emit ClaimVerified(result.claims[i]);
}
+ _verifiedSignedMessage =result.signedMessage;
+ emit SignedMessageVerified(result.signedMessage);
}
+
function getVerifiedClaims() external view returns (VerifiedClaim[] memory) {
return _verifiedClaims;
}
@@ -107,4 +96,17 @@ contract Airdrop is ERC20, SismoConnect {
function getVerifiedSignedMessage() external view returns (bytes memory) {
return _verifiedSignedMessage;
}
+
+ function _setAuths(AuthRequest[] memory auths) private {
+ for (uint256 i = 0; i < auths.length; i++) {
+ _authRequests.push(auths[i]);
+ }
+ }
+
+ function _setClaims(ClaimRequest[] memory claims) private {
+ for (uint256 i = 0; i < claims.length; i++) {
+ _claimRequests.push(claims[i]);
+ }
+ }
+
}