Signature on Client
Author of this section: @LiKang
signing and checking are important features. This section describes how to implement a signature on the client and verify it on the server.
What's the signature?
In DApp, the user system is usually built based on the blockchain address, and a blockchain address represents a user. In traditional applications, we usually use passwords, mobile phone verification codes and other methods to verify users. So in DApp, how do we verify that the operator is indeed the owner of a blockchain address?
In the previous course, we implemented the connection of a block chain address by evoking the user's wallet, so that the address information can be obtained in the DApp. Does this prove that the user owns this address? Can we allow users to operate related assets in DApp after connecting the user address?
If the asset is on the blockchain, that may be possible, because smart contract calls require private key signature authentication corresponding to the address. However, not all assets are on the chain. If your DApp needs to operate user assets in the traditional database, you must ensure that the user currently operating has the relevant permissions.
However, it is unreliable to think that the user has the account just by connecting the wallet to obtain the address, because the interface that calls the wallet to obtain the address may be forged by the client. Therefore, we need to let the user verify his identity through signature. The user signs a message through his private key, and the DApp server verifies the signature result through the public key, so as to ensure the user's operation permission.
In this case, let's implement a simple example that can be signed to verify identity after connecting to the wallet:
implement front-end signatures
Let's implement the front-end logic first, and we will implement it quickly based on the previous courses. Connect Wallet .
Create a new one pages/sign/index.tsx
file, copy the previous code, and then make the next modification, create a new one components/SignDemo
components:
import React from 'react';
- import { Address, ConnectButton, Connector, NFTCard } from "@ant-design/web3";
import { MetaMask, WagmiWeb3ConfigProvider } from "@ant-design/web3-wagmi";
import { createConfig, http } from 'wagmi';
import { injected } from "wagmi/connectors";
import { mainnet } from 'wagmi/chains';
+ import SignDemo from '../../components/SignDemo';
const config = createConfig({
chains: [mainnet],
transports: {
[mainnet.id]: http(),
},
connectors: [
injected({
target: "metaMask",
}),
],
});
const Demo:React.FC = () => {
return (
<WagmiWeb3ConfigProvider eip6963 config={config} wallets={[MetaMask()]}>
+ <SignDemo />
- <Address format address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9" />
- <NFTCard
- address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9"
- tokenId={641}
- />
- <Connector>
- <ConnectButton />
- </Connector>
</WagmiWeb3ConfigProvider>
);
}
export default Demo;
then in SignDemo
write a basic link wallet button inside the component, the code is as follows:
import React from "react";
import { ConnectButton, Connector } from "@ant-design/web3";
const SignDemo: React.FC = () => {
return (
<Connector>
<ConnectButton />
</Connector>
);
};
export default SignDemo;
in this way, we have implemented the basic connection logic.
The signature part logic is then supplemented, first introducing wagmi
of useSignMessage
and Ant Design Web3 useAccount
hooks, implementation doSignature
:
import React from "react";
- import { ConnectButton, Connector } from "@ant-design/web3";
+ import { ConnectButton, Connector, useAccount } from "@ant-design/web3";
+ import { useSignMessage } from "wagmi";
+ import { message } from "antd";
const SignDemo: React.FC = () => {
+ const { signMessageAsync } = useSignMessage();
+ const { account } = useAccount();
+ const doSignature = async () => {
+ try {
+ const signature = await signMessageAsync({
+ message: "test message for WTF-DApp demo",
+ });
+ } catch (error: any) {
+ message.error(`Signature failed: ${error.message}`);
+ }
+ };
return (
<Connector>
<ConnectButton />
</Connector>
);
};
export default SignDemo;
let's add a button, click the button and call doSignature
method, we set up disabled
property, the signature can be called only after the connection has been successful:
import React from "react";
import { ConnectButton, Connector, useAccount } from "@ant-design/web3";
import { useSignMessage } from "wagmi";
- import { message } from "antd";
+ import { message, Space, Button } from "antd";
const SignDemo: React.FC = () => {
// ...
return (
+ <Space>
<Connector>
<ConnectButton />
</Connector>
+ <Button
+ disabled={!account?.address}
+ onClick={doSignature}
+ >
+ Sign message
+ </Button>
+ </Space>
);
};
export default SignDemo;
In this way, we implement the logic of the front-end signature, but as mentioned earlier, the signature needs to be sent to the server before verification, so we need to implement the server-end signature verification interface first.
Realize the service side check and sign
the back-end signature is generally relied on. viem
or ethers
such as library. You can create a new one directly. /pages/api/signatureCheck.ts
file, Next.js will automatically /api
the file under is run as the back end. Vercel Function processing.
We are based on viem
implementation:
// /pages/api/signatureCheck.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
export const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const body = req.body;
const valid = await publicClient.verifyMessage({
address: body.address,
message: "test message for WTF-DApp demo",
signature: body.signature,
});
res.status(200).json({ data: valid });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
}
if better ethers
can be replaced by the following code implementation:
const verifyMessage = async (signerAddress, signature) => {
const recoveredAddress = ethers.utils.verifyMessage(
"test message for WTF-DApp demo",
signature
);
return recoveredAddress === signerAddress;
};
front-end call interface check sign
finally, we will supplement the logic of the front-end call interface. You can copy the following code directly into SignDemo
in component:
const checkSignature = async (params: {
address?: string;
signature: string;
}) => {
try {
const response = await fetch("/api/signatureCheck", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
});
const result = await response.json();
if (result.data) {
message.success("Signature success");
} else {
message.error("Signature failed");
}
} catch (error) {
message.error("An error occurred");
}
};
and then we're in doSignature
call this method in the method and add a Loading state:
import React from "react";
import { ConnectButton, Connector, useAccount } from "@ant-design/web3";
import { useSignMessage } from "wagmi";
import { message, Space, Button } from "antd";
const SignDemo: React.FC = () => {
const { signMessageAsync } = useSignMessage();
const { account } = useAccount();
+ const [signLoading, setSignLoading] = React.useState(false);
const doSignature = async () => {
+ setSignLoading(true);
try {
const signature = await signMessageAsync({
message: "test message for WTF-DApp demo",
});
+ await checkSignature({
+ address: account?.address,
+ signature,
+ });
} catch (error: any) {
message.error(`Signature failed: ${error.message}`);
}
+ setSignLoading(false);
};
// checkSignature here
return (
<Space>
<Connector>
<ConnectButton />
</Connector>
<Button
+ loading={signLoading}
disabled={!account?.address}
onClick={doSignature}
>
Sign message
</Button>
</Space>
);
};
export default SignDemo;
Complete code you can in sign Directory found in.