Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Created April 25, 2024 16:52
Show Gist options
  • Save mattdesl/2e94c0258d97cd8a38fb345edb058b74 to your computer and use it in GitHub Desktop.
Save mattdesl/2e94c0258d97cd8a38fb345edb058b74 to your computer and use it in GitHub Desktop.
wallet signature mechanism for Meridian book verification
import { ConnectButton } from '@rainbow-me/rainbowkit';
import type { NextPage } from 'next';
import { useSignMessage } from 'wagmi'
import Image from 'next/image';
import { verifyMessage } from 'ethers/lib/utils'
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { createMessage } from '../tools/sign';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useRef, useState } from 'react';
import bookImage from '../public/images/book.jpg';
const shortAddress = (addr:string) => `${addr.slice(0, 4)}...${addr.slice(-4)}`;
const Sign = () => {
const [response, setResponse] = useState<any>(null);
const { isConnected, address } = useAccount();
// const recoveredAddress = useRef<string>();
const { data, error, isLoading, signMessage } = useSignMessage({
onError (error) {
console.warn(error)
setResponse(null);
},
async onSuccess(signature, { message }) {
console.log('signature', isConnected);
if (!isConnected) {
setResponse({ error: 'disconnected' });
return;
}
// Verify signature when sign message succeeds
const signedAddress = verifyMessage(message, signature)
if (signedAddress !== address) {
setResponse({ error: 'Address mismatch' })
} else {
const resp = await fetch('/api/verify?signature=' + signature)
const json = await resp.json();
setResponse(json);
}
},
});
if (!isConnected) {
return <div>Please sign a message...</div>;
} else if (response) {
if (response.discounts) {
return <div>
<div>Address <code>{shortAddress(response.address)}</code> can claim <strong><code>{response.discounts.length}</code></strong> discount {
response.discounts.length === 1 ? 'code' : 'codes'
}:</div>
<ul>
{response.discounts.map((d:any, i:number) => {
const href = `https://www.vetroeditions.com/discount/${d.discount}?redirect=%2Fproducts%2Fmeridian`;
return <div key={i}>Meridian #{parseInt(d.tokenId, 10)-163000000} — Discount Code: <code>
<a href={href} target="_blank" rel="noreferrer">{d.discount}</a>
</code></div>
})}
</ul>
</div>
} else if (response.progress) {
return <button className={styles.signButton} disabled>Waiting for Signature...</button>
} else {
if (response.error && response.code === 'NOT_ELIGIBLE') {
return <div>
<h2 className={styles.signError}>Error</h2>
<p>The connected wallet <code>{shortAddress(address)}</code> held no Meridian tokens at the time of snapshot. Are you sure you signed with the right wallet?</p>
</div>
} else {
return <div className={styles.signError}>ERROR: {response.error}</div>
}
}
} else {
return <button className={styles.signButton} onClick={() => {
setResponse({ progress: 'signing' });
signMessage({ message: createMessage() })
}}>Sign Message</button>
}
}
const SignContainer = () => {
const { isConnected, address } = useAccount()
if (!isConnected) {
return <div className={styles.signContainer}></div>
} else {
return <div className={styles.signContainer}>
<Sign />
</div>
}
}
const Home: NextPage = () => {
const { isConnected, address } = useAccount()
console.log('Connected address', address)
return (
<div className={styles.container}>
<Image src={bookImage} alt='Meridian book' />
<header className={styles.header}>Meridian Book Discount (Verification)</header>
<p>
If you hold a <a href="https://archipelago.art/collections/meridian" target="_blank" rel="noreferrer">Meridian token by Matt DesLauriers</a>, you are eligible to receive a discount code to purchase the Meridian book on <a href="https://www.vetroeditions.com/products/meridian" target="_blank" rel="noreferrer">Vetro Editions</a> at no cost except shipping.
Connect your wallet below and then sign a message to verify ownership.
</p>
<ConnectButton showBalance={false} />
<SignContainer />
<hr className={styles.hr} />
<h3>Details</h3>
<ul>
...
</ul>
</div>
);
};
Home.displayName = 'Home';
export default Home;
export function createMessage () {
return `Signing a message to prove that this wallet owns one or more Meridian tokens by Matt DesLauriers.
This message is being used to redeem a discount code for the Meridian book project.`
}
// Note this must occur on backend
import { ethers } from 'ethers';
import { createMessage } from '../../tools/sign';
import snapshot from '../snapshots/snapshot.json';
import discounts from './discounts.json';
const tokens = snapshot.tokens;
const ownerMap = new Map();
discounts.forEach((discount, id) => {
const token = tokens[id];
const address = token.owner.id.toLowerCase();
if (!ownerMap.has(address)) {
ownerMap.set(address, []);
}
const discounts = ownerMap.get(address);
discounts.push({ tokenId: token.tokenId, discount });
});
const ownerAliasMap = new Map();
// ownerAliasMap.set("0x32262672C6D1B814019f4Ca4e2fc53285a919704", "0xcab81f14a3fc98034a05bab30f8d0e53e978c833")
// console.log(ownerMap.get("0xcab81f14a3fc98034a05bab30f8d0e53e978c833"))
export default function handler(req, res) {
if (!req.query || !req.query.signature) {
return res.status(400).send({ error: 'No signature query provided' });
}
const signature = String(req.query.signature || '');
const message = createMessage();
let address;
try {
address = ethers.utils.verifyMessage(message, signature).toLowerCase();
if (ownerAliasMap.has(address)) {
address = ownerAliasMap.get(address).toLowerCase();
}
} catch (err) {
return res.status(400).send({ error: 'Signature request was not properly formed' });
}
if (address && ownerMap.has(address)) {
res.send({ address, discounts: ownerMap.get(address) });
} else {
res.send({ error: 'Not in address set', code: 'NOT_ELIGIBLE' });
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment