Created
April 25, 2024 16:52
-
-
Save mattdesl/2e94c0258d97cd8a38fb345edb058b74 to your computer and use it in GitHub Desktop.
wallet signature mechanism for Meridian book verification
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.` | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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