From 59eaabaee4d881d59ba49568841fdf43cf718315 Mon Sep 17 00:00:00 2001 From: Julien Genestoux Date: Wed, 26 Feb 2025 15:27:38 -0500 Subject: [PATCH 1/5] wip --- airdrops/components/CampaignCard.tsx | 13 +- airdrops/components/CampaignDetailContent.tsx | 16 +- airdrops/components/Campaigns.tsx | 12 +- airdrops/package.json | 2 + airdrops/src/airdrops.json | 24 ++- airdrops/src/utils/eligibility.ts | 30 ++-- airdrops/src/utils/terms.ts | 169 ++++++++++++++++++ yarn.lock | 94 +++++++++- 8 files changed, 317 insertions(+), 43 deletions(-) create mode 100644 airdrops/src/utils/terms.ts diff --git a/airdrops/components/CampaignCard.tsx b/airdrops/components/CampaignCard.tsx index 5f9a2d47b5c..bb3b29563b4 100644 --- a/airdrops/components/CampaignCard.tsx +++ b/airdrops/components/CampaignCard.tsx @@ -8,7 +8,7 @@ interface CampaignCardProps { } const CampaignCardInternal = ({ - airdrop: { title, description, eligible }, + airdrop: { title, description, eligible, contractAddress, url }, authenticated, }: CampaignCardProps) => { return ( @@ -16,7 +16,7 @@ const CampaignCardInternal = ({

{title}

{description}

- {authenticated && ( + {authenticated && contractAddress && ( <>
)} + {url && ( + + More info + + )} ) diff --git a/airdrops/components/CampaignDetailContent.tsx b/airdrops/components/CampaignDetailContent.tsx index 94303de7c43..d5615c145cc 100644 --- a/airdrops/components/CampaignDetailContent.tsx +++ b/airdrops/components/CampaignDetailContent.tsx @@ -9,6 +9,8 @@ import { BsArrowLeft as ArrowBackIcon } from 'react-icons/bs' import { ConnectButton } from './auth/ConnectButton' import { isEligible } from '../src/utils/eligibility' import { AirdropData } from './Campaigns' +import ReactMarkdown from 'react-markdown' +import { terms } from '../src/utils/terms' interface CampaignDetailContentProps { airdrop: AirdropData @@ -25,10 +27,7 @@ export default function CampaignDetailContent({ useEffect(() => { const run = async () => { - const amount = await isEligible( - wallets[0].address, - airdrop.recipientsFile - ) + const amount = await isEligible(wallets[0].address, airdrop) airdrop.eligible = amount || 0 } run() @@ -90,12 +89,9 @@ export default function CampaignDetailContent({

Terms of Service

-

- Lorem ipsum, dolor sit amet consectetur adipisicing elit. Earum - excepturi id explicabo, ad iste, autem placeat expedita aliquid, - commodi qui nam fuga asperiores ab fugit ducimus ipsam. Libero, - pariatur. Possimus? -

+
+ +
diff --git a/airdrops/components/Campaigns.tsx b/airdrops/components/Campaigns.tsx index 4ba0b53752b..26fa5b8dc1d 100644 --- a/airdrops/components/Campaigns.tsx +++ b/airdrops/components/Campaigns.tsx @@ -14,8 +14,11 @@ export interface AirdropData { title: string description: string contractAddress?: string - tokenAmount: string - tokenSymbol: string + token: { + address: string + symbol: string + decimals: number + } recipientsFile: string eligible?: number } @@ -53,10 +56,7 @@ const CampaignsContent = () => { await Promise.all( (airdrops as AirdropData[]).map(async (drop) => { - const amount = await isEligible( - user.wallet.address, - drop.recipientsFile - ) + const amount = await isEligible(user.wallet.address, drop) drop.eligible = amount || 0 }) ) diff --git a/airdrops/package.json b/airdrops/package.json index f51a7341c96..624de32a094 100644 --- a/airdrops/package.json +++ b/airdrops/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@headlessui/react": "2.1.9", + "@openzeppelin/merkle-tree": "1.0.8", "@privy-io/react-auth": "2.2.1", "@sentry/nextjs": "8.54.0", "@tanstack/react-query": "5.59.19", @@ -26,6 +27,7 @@ "ethers": "6.13.5", "next": "14.2.21", "react-hot-toast": "2.4.1", + "react-markdown": "10.0.0", "tailwind-merge": "3.0.1", "typescript": "5.6.3" }, diff --git a/airdrops/src/airdrops.json b/airdrops/src/airdrops.json index 807c78d8f5d..a7ba5200149 100644 --- a/airdrops/src/airdrops.json +++ b/airdrops/src/airdrops.json @@ -3,17 +3,23 @@ "id": "1", "title": "UP Token Swap", "description": "The Unlock DAO migration to Base is complete, and the UP token swap reward airdrop, totaling 1.061 million UP tokens, is now live for all eligible participants.", - "tokenAmount": "1061000", - "tokenSymbol": "UP", - "recipientsFile": "https://example.com/airdrop1-recipients.json" + "url": "https://unlock-protocol.com/blog/up-token-swap-reward-airdrop-now-live-" }, { "id": "2", - "title": "Airdrop #2", - "description": "More to come", - "contractAddress": "0x1234567890123456789012345678901234567890", - "tokenAmount": "10000", - "tokenSymbol": "UP", - "recipientsFile": "https://example.com/airdrop2-recipients.json" + "title": "Blastoff Airdrop", + "description": "The Unlock Protocol Foundation is launching a next airdrop to Unlock Protocol community members, distributing over 7 million $UP tokens on Base to over 10,000 members of the community!", + "contractAddress": "0x3b26D06Ea8252a73742d2125D1ACEb594ECEE5c6", + "recipientsFile": "https://merkle-trees.unlock-protocol.com/0xe238effc14b43022c9ce132e22f0baa73cdd8696f4b435150a4c9341c83abfbf.json", + "token": { + "address": "0x3b26D06Ea8252a73742d2125D1ACEb594ECEE5c6", + "symbol": "UP", + "decimals": 18 + } + }, + { + "id": "3", + "title": "Trading Volume", + "description": "More details to be announced soon!" } ] diff --git a/airdrops/src/utils/eligibility.ts b/airdrops/src/utils/eligibility.ts index 54a60bfd70f..486a3c40b10 100644 --- a/airdrops/src/utils/eligibility.ts +++ b/airdrops/src/utils/eligibility.ts @@ -1,21 +1,25 @@ +import { ethers } from 'ethers' +import { AirdropData } from '../../components/Campaigns' + /** * Checks if an address is eligible for an airdrop and returns the token amount * This is a temporary implementation that randomly determines eligibility * To be replaced with actual implementation that checks against the recipients file */ export const isEligible = async ( - _address: string, - _recipientsFile: string + address: string, + airdrop: AirdropData ): Promise => { - // Temporary implementation: randomly determine eligibility - // const random = Math.random() - - // // 40% chance of being eligible - // if (random < 0.4) { - // // Random amount between 100 and 1000 tokens - // const amount = Math.floor(Math.random() * 900) + 100 - // return amount - // } - - return 1337 + if (!airdrop.recipientsFile || !address) { + return 0 + } + const request = await fetch(airdrop.recipientsFile) + const recipients = await request.json() + const recipient = recipients.values.find((recipient: any) => { + return recipient.value[0] === address + }) + if (!recipient) { + return 0 + } + return Number(ethers.formatUnits(recipient.value[1], airdrop.token.decimals)) } diff --git a/airdrops/src/utils/terms.ts b/airdrops/src/utils/terms.ts new file mode 100644 index 00000000000..f662584c40c --- /dev/null +++ b/airdrops/src/utils/terms.ts @@ -0,0 +1,169 @@ +export const terms = ` +**Unlock Protocol Foundation** + +**Airdrop Terms and Conditions** + +Effective Date: February 2025 + +Last Updated: February 2025 + +These Airdrop Terms and Conditions (“**Terms**”) govern your participation in the airdrop program (the “**Airdrop**”) offered by Unlock Protocol Foundation (“**we**,” “**us**,” or “**our**”). The purpose of the Airdrop is to distribute Unlock Protocol (UP) (“**Tokens**”) to eligible participants as part of our broader ecosystem growth and governance initiative. By claiming, receiving, or using any Tokens distributed through the Airdrop, you acknowledge that you have read, understood, and agreed to be bound by these Terms. If you do not agree with any part of these Terms, you must not participate in the Airdrop. + +For the purposes of these Terms, references to our “Website” refer to [https://airdrops.unlock-protocol.com/](https://airdrops.unlock-protocol.com/) + +1. **Eligibility** + +To participate in the Airdrop, you must: + +1. Be at least 18 years old and legally capable of entering into a binding contract. + + 2. Not be a resident or entity located in any jurisdiction subject to comprehensive sanctions or embargoes, including but not limited to North Korea, Iran, Syria, Cuba, and the Crimea, Donetsk, and Luhansk regions of Ukraine. + + 3. Not appear on any sanctions list maintained by the U.S. Office of Foreign Assets Control (OFAC), the UK Office of Financial Sanctions Implementation (OFSI), the European Union, the United Nations Security Council, or any other relevant sanctioning body. + + 4. Have your wallet address screened against relevant sanctions lists before receiving Tokens. We reserve the right to deny participation if your wallet is flagged for any sanctions violations. + + 5. Comply with all applicable laws and regulations governing your participation in the Airdrop. + +2) **Nature of Tokens** + + 1. The Tokens distributed through the Airdrop are not an investment and do not represent equity, shares, or any ownership interest in Unlock Protocol Foundation or its affiliates. + + 2. The Tokens do not provide holders with any rights to dividends, profits, or other financial returns. + + 3. We make no representations or warranties regarding the future value or liquidity of the Tokens. + +3) **Old and New Airdrop Provisions** + + 1. The Airdrop consists of both a new distribution and a prior distribution of Tokens completed on November 1st 2024\. + + 2. If you previously received Tokens under the prior airdrop, you must acknowledge and accept these Terms as applicable to the prior airdrop before claiming further distributions. + + 3. The new Airdrop is governed by these Terms, and participation requires explicit acceptance. + + 4. If you were eligible but did not claim Tokens in the prior Airdrop, you must confirm acceptance of these updated Terms before claiming any unclaimed distributions. + + 5. The Foundation reserves the right to revoke eligibility for any participant found to have violated these Terms in connection with either the prior or current Airdrop. + + 6. To mitigate risks, participants who were eligible for previous distributions may be required to check an additional confirmation box to acknowledge the past distribution terms. + + 7. If an airdrop recipient disagrees with the basis of the prior airdrop, they may return the Tokens, and failure to do so will be considered acceptance of these Terms. + + 8. The Foundation reserves the right to block users from future distributions if they are found to have circumvented terms or engaged in prohibited actions. + + 9. Wallet addresses from both the prior and current Airdrop will be screened for sanctions compliance, and any flagged wallets may be disqualified from participation and may be required to return the Tokens. + +4) **Claiming the Airdrop** + + 1. To claim the Airdrop, you may be required to interact with the user interface on our Website and explicitly accept these Terms. + + 2. You may need to connect a compatible blockchain wallet to receive the Tokens. + + 3. Participants acknowledge that transactions on blockchain networks are irreversible, and lost or misdirected Tokens cannot be recovered. + + 4. Claiming through any method other than the designated front-end interface may forfeit eligibility. + + 5. The Foundation may implement measures to store proof of agreement, ensuring compliance in the event of regulatory scrutiny. + +5) **Restrictions on Use** + +You agree not to: + +1. Use the Tokens for illegal purposes, including but not limited to money laundering, terrorism financing, or fraudulent activities. + + 2. Circumvent geographic, legal, or other restrictions, such as by using VPNs or similar tools. + + 3. Engage in activities that could damage, disable, or impair the integrity of the Airdrop process. + +6) **No Guarantees & Limitation of Liability** + + 1. We do not guarantee the availability, functionality, or uninterrupted operation of the Airdrop or associated smart contracts. + + 2. The Airdrop is provided on an “as-is” and “as available” basis without warranties of any kind. + + 3. To the fullest extent permitted by law, we disclaim all liability for any direct, indirect, or consequential losses arising from participation in the Airdrop, including but not limited to: + + 1. Loss of access to your wallet or Tokens. + + 2. Technical failures or vulnerabilities in blockchain networks. + + 3. Regulatory changes impacting the Tokens or Airdrop. + +7) **Compliance & Fraud Prevention** + + 1. We reserve the right to require identity verification from participants if deemed necessary. + + 2. If we determine that you have engaged in fraudulent activities or violated these Terms, we reserve the right to revoke your eligibility and recover any Tokens distributed. + + 3. If any participant is later found to be in violation of applicable sanctions laws, we may take necessary actions, including freezing or reclaiming the Tokens where legally permissible. + + 4. To maintain compliance, the Foundation may audit distributions and request documentation from participants to verify adherence to these Terms. + + 5. Wallet addresses will be screened prior to distribution, and any flagged addresses may be disqualified. + +8) **Acknowledgment of Risks** + + 1. By participating in the Airdrop, you acknowledge and accept the following risks: + + 1. Blockchain-based transactions are permanent and irreversible. + + 2. The Tokens may have no value and may not be redeemable for any assets or services. + + 3. Regulatory developments could impact the legality and usability of the Tokens in your jurisdiction. + +9) **Amendments & Termination** + + 1. We reserve the right to modify or terminate the Airdrop and these Terms at any time without prior notice. + + 2. Changes will be effective upon posting the updated Terms on our Website. + + 3. Your continued participation in the Airdrop constitutes acceptance of any modifications. + +10) **Governing Law & Dispute Resolution** + + 1. These Terms shall be governed by and construed in accordance with the laws of the Cayman Islands. + + 2. Any disputes arising out of or in connection with these Terms shall be subject to binding arbitration in the Cayman Islands under the London Court of International Arbitration Rules. + +11) **Contact Information** + +If you have any questions regarding these Terms, please contact us at: + +Email: [hello@unlock-protocol.com](mailto:hello@unlock-protocol.com) + +Website: https://airdrops.unlock-protocol.com/ + +12. **User Acknowledgement** + +By participating in the Airdrop, including both prior and current distributions, and using the associated Website, you expressly confirm and agree to the following: + +1. **Knowledge and Understanding** + +You affirm that you possess sufficient knowledge and understanding to interact with blockchain-based systems, digital assets, and related technologies in a responsible manner, including but not limited to the mechanics of token distribution, wallet security, and transaction finality on the blockchain. + +2. **Assumption of Risk** + +You acknowledge and assume full responsibility for understanding and managing the risks outlined in these Terms, including but not limited to potential token volatility, smart contract vulnerabilities, regulatory developments, and the irreversible nature of blockchain transactions. + +3. **Third-Party Services** + +You understand that the Airdrop, including both prior and current distributions, may involve third-party platforms, such as blockchain explorers, wallet providers, and smart contract interfaces, which are outside of the control of the Unlock Protocol Foundation. Your use of these services is solely at your own risk and subject to their respective terms and conditions. + +4. **Acceptance of Terms** + +By claiming or receiving Tokens through the prior or current Airdrop, you confirm that you have read, understood, and accepted these Terms, including all limitations of liability, disclaimers, compliance requirements, and responsibilities outlined herein. + +5. **No Expectation of Profits or Returns** + +You acknowledge that the Tokens received through the prior or current Airdrop do not constitute an investment, carry no inherent financial return, and provide no right to dividends, revenue sharing, or governance beyond their stated functional purpose. + +6. **Sanctions and Compliance** + +You acknowledge that your wallet address will be screened against applicable sanctions lists and that participation in the Airdrop, including prior and current distributions, is prohibited for individuals or entities subject to sanctions restrictions. The Unlock Protocol Foundation reserves the right to deny or revoke participation if any compliance risks are identified. + +7. **Retroactive Acceptance for Prior Airdrop** + +If you previously received Tokens through a prior Airdrop, you expressly acknowledge that these Terms apply retroactively to that distribution. If you do not agree, you must return any Tokens received through the prior Airdrop. Failure to do so constitutes acceptance of these Terms. + +By participating in the Airdrop, you confirm that you have read, understood, and agreed to these Terms. +` diff --git a/yarn.lock b/yarn.lock index 2e8024c2756..f89dd7d9997 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6097,7 +6097,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/tx@npm:^4.1.2": +"@ethereumjs/tx@npm:^4.1.2, @ethereumjs/tx@npm:^4.2.0": version: 4.2.0 resolution: "@ethereumjs/tx@npm:4.2.0" dependencies: @@ -8453,6 +8453,16 @@ __metadata: languageName: node linkType: hard +"@metamask/abi-utils@npm:^2.0.4": + version: 2.0.4 + resolution: "@metamask/abi-utils@npm:2.0.4" + dependencies: + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^9.0.0" + checksum: 10/3d32d42c6e98fc4719b2b53597e573764b80936c7cc31d884c87729c4c4f74a30e93096db87aaa7cbcec9d3bb7d22b1adfc98a8bcb4c7c2f17bfbddaa4367d34 + languageName: node + linkType: hard + "@metamask/eth-sig-util@npm:^4.0.0": version: 4.0.1 resolution: "@metamask/eth-sig-util@npm:4.0.1" @@ -8481,6 +8491,13 @@ __metadata: languageName: node linkType: hard +"@metamask/superstruct@npm:^3.1.0": + version: 3.1.0 + resolution: "@metamask/superstruct@npm:3.1.0" + checksum: 10/5066fe228d5f11da387606d7f9545de2b473ab5a9e0f1bb8aea2f52d3e2c9d25e427151acde61f4a2de80a07a9871fe9505ad06abca6a61b7c3b54ed5c403b01 + languageName: node + linkType: hard + "@metamask/utils@npm:^3.4.1": version: 3.6.0 resolution: "@metamask/utils@npm:3.6.0" @@ -8506,6 +8523,23 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^9.0.0": + version: 9.3.0 + resolution: "@metamask/utils@npm:9.3.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + debug: "npm:^4.3.4" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/ed6648cd973bbf3b4eb0e862903b795a99d27784c820e19f62f0bc0ddf353e98c2858d7e9aaebc0249a586391b344e35b9249d13c08e3ea0c74b23dc1c6b1558 + languageName: node + linkType: hard + "@motionone/animation@npm:^10.15.1, @motionone/animation@npm:^10.18.0": version: 10.18.0 resolution: "@motionone/animation@npm:10.18.0" @@ -9894,7 +9928,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.7.1, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.7.1": +"@noble/hashes@npm:1.7.1, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.7.1": version: 1.7.1 resolution: "@noble/hashes@npm:1.7.1" checksum: 10/ca3120da0c3e7881d6a481e9667465cc9ebbee1329124fb0de442e56d63fef9870f8cc96f264ebdb18096e0e36cebc0e6e979a872d545deb0a6fed9353f17e05 @@ -12478,6 +12512,16 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/merkle-tree@npm:1.0.8": + version: 1.0.8 + resolution: "@openzeppelin/merkle-tree@npm:1.0.8" + dependencies: + "@metamask/abi-utils": "npm:^2.0.4" + ethereum-cryptography: "npm:^3.0.0" + checksum: 10/dc53789b852e164a87f181a9141c3e26160bba2431c685dff0ca07409e7d051f49ae65308e9422d0a75db747a7646dff3dc756fb66f3fcd4818108dcd0c9b74f + languageName: node + linkType: hard + "@openzeppelin/upgrades-core@npm:1.39.0": version: 1.39.0 resolution: "@openzeppelin/upgrades-core@npm:1.39.0" @@ -13970,7 +14014,7 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.1, @scure/base@npm:~1.2.2, @scure/base@npm:~1.2.4": +"@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.3, @scure/base@npm:~1.2.2, @scure/base@npm:~1.2.4": version: 1.2.4 resolution: "@scure/base@npm:1.2.4" checksum: 10/4b61679209af40143b49ce7b7570e1d9157c19df311ea6f57cd212d764b0b82222dbe3707334f08bec181caf1f047aca31aa91193c678d6548312cb3f9c82ab1 @@ -18572,6 +18616,7 @@ __metadata: resolution: "@unlock-protocol/airdrops@workspace:airdrops" dependencies: "@headlessui/react": "npm:2.1.9" + "@openzeppelin/merkle-tree": "npm:1.0.8" "@privy-io/react-auth": "npm:2.2.1" "@sentry/nextjs": "npm:8.54.0" "@tanstack/react-query": "npm:5.59.19" @@ -18596,6 +18641,7 @@ __metadata: next: "npm:14.2.21" postcss: "npm:8.4.49" react-hot-toast: "npm:2.4.1" + react-markdown: "npm:10.0.0" tailwind-merge: "npm:3.0.1" tailwindcss: "npm:3.4.17" typescript: "npm:5.6.3" @@ -29130,6 +29176,19 @@ __metadata: languageName: node linkType: hard +"ethereum-cryptography@npm:^3.0.0": + version: 3.1.0 + resolution: "ethereum-cryptography@npm:3.1.0" + dependencies: + "@noble/ciphers": "npm:1.2.1" + "@noble/curves": "npm:1.8.1" + "@noble/hashes": "npm:1.7.1" + "@scure/bip32": "npm:1.6.2" + "@scure/bip39": "npm:1.5.4" + checksum: 10/97345df6afb4b42da51cdbbbc6c2c3861bfb3f7b63ee64c62b878d1110fe28b7e0ca95f6c17f78bad22f07ba4f519f3d901432d26842cd64255cb5cc7224b49d + languageName: node + linkType: hard + "ethereumjs-abi@npm:^0.6.8": version: 0.6.8 resolution: "ethereumjs-abi@npm:0.6.8" @@ -42274,6 +42333,13 @@ __metadata: languageName: node linkType: hard +"pony-cause@npm:^2.1.10": + version: 2.1.11 + resolution: "pony-cause@npm:2.1.11" + checksum: 10/ed7d0bb6e3e69f753080bf736b71f40e6ae4c13ec0c8c473ff73345345c088819966fdd68a62ad7482d464bf41176cf9421f5f63715d1a4532005eedc099db55 + languageName: node + linkType: hard + "posix-character-classes@npm:^0.1.0": version: 0.1.1 resolution: "posix-character-classes@npm:0.1.1" @@ -44515,6 +44581,28 @@ __metadata: languageName: node linkType: hard +"react-markdown@npm:10.0.0": + version: 10.0.0 + resolution: "react-markdown@npm:10.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + devlop: "npm:^1.0.0" + hast-util-to-jsx-runtime: "npm:^2.0.0" + html-url-attributes: "npm:^3.0.0" + mdast-util-to-hast: "npm:^13.0.0" + remark-parse: "npm:^11.0.0" + remark-rehype: "npm:^11.0.0" + unified: "npm:^11.0.0" + unist-util-visit: "npm:^5.0.0" + vfile: "npm:^6.0.0" + peerDependencies: + "@types/react": ">=18" + react: ">=18" + checksum: 10/513d9119587b561ac1195e3b2fa30b2bd9149b4dea658c189e2417fda600fbf9d5067967f098ea8e2d525b5fc7db00e3e4176ba2662a6eb503bc09d9b8bd5b5a + languageName: node + linkType: hard + "react-markdown@npm:9.0.3": version: 9.0.3 resolution: "react-markdown@npm:9.0.3" From e78157cfca877447fc3bab01b1f306f54c3deb3a Mon Sep 17 00:00:00 2001 From: Julien Genestoux Date: Wed, 26 Feb 2025 15:46:47 -0500 Subject: [PATCH 2/5] more work --- airdrops/components/CampaignDetailContent.tsx | 29 +++++++++---------- airdrops/src/utils/terms.ts | 4 +-- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/airdrops/components/CampaignDetailContent.tsx b/airdrops/components/CampaignDetailContent.tsx index d5615c145cc..00669181dc2 100644 --- a/airdrops/components/CampaignDetailContent.tsx +++ b/airdrops/components/CampaignDetailContent.tsx @@ -27,8 +27,10 @@ export default function CampaignDetailContent({ useEffect(() => { const run = async () => { - const amount = await isEligible(wallets[0].address, airdrop) - airdrop.eligible = amount || 0 + if (wallets[0]) { + const amount = await isEligible(wallets[0].address, airdrop) + airdrop.eligible = amount || 0 + } } run() }, [authenticated, wallets, airdrop]) @@ -69,6 +71,11 @@ export default function CampaignDetailContent({ } } + const onClaim = async () => { + console.log('Claiming tokens for', airdrop.contractAddress) + console.log({ termsOfServiceSignature, timestamp }) + } + return ( diff --git a/airdrops/src/utils/terms.ts b/airdrops/src/utils/terms.ts index f662584c40c..717143c6da6 100644 --- a/airdrops/src/utils/terms.ts +++ b/airdrops/src/utils/terms.ts @@ -1,7 +1,5 @@ export const terms = ` -**Unlock Protocol Foundation** - -**Airdrop Terms and Conditions** +## Airdrop Terms and Conditions Effective Date: February 2025 From 53a867cdd194db242e99741c9add4ccdf16d627c Mon Sep 17 00:00:00 2001 From: Julien Genestoux Date: Wed, 26 Feb 2025 16:21:48 -0500 Subject: [PATCH 3/5] adding campaign --- airdrops/app/campaigns/[campaign]/page.tsx | 6 +- airdrops/components/CampaignCard.tsx | 4 +- airdrops/components/CampaignDetailContent.tsx | 64 ++++++++++++++++--- airdrops/components/Campaigns.tsx | 8 ++- airdrops/package.json | 1 + airdrops/src/airdrops.json | 9 +-- yarn.lock | 3 +- 7 files changed, 73 insertions(+), 22 deletions(-) diff --git a/airdrops/app/campaigns/[campaign]/page.tsx b/airdrops/app/campaigns/[campaign]/page.tsx index fbac828783e..b39283350a4 100644 --- a/airdrops/app/campaigns/[campaign]/page.tsx +++ b/airdrops/app/campaigns/[campaign]/page.tsx @@ -22,15 +22,15 @@ export async function generateMetadata({ } return { - title: `${campaign.title} | Airdrops`, + title: `${campaign.name} | Airdrops`, description: campaign.description, openGraph: { - title: `${campaign.title} | Airdrops`, + title: `${campaign.name} | Airdrops`, description: campaign.description, }, twitter: { card: 'summary', - title: `${campaign.title} | Airdrops`, + title: `${campaign.name} | Airdrops`, description: campaign.description, }, } diff --git a/airdrops/components/CampaignCard.tsx b/airdrops/components/CampaignCard.tsx index bb3b29563b4..bcb643f6823 100644 --- a/airdrops/components/CampaignCard.tsx +++ b/airdrops/components/CampaignCard.tsx @@ -8,12 +8,12 @@ interface CampaignCardProps { } const CampaignCardInternal = ({ - airdrop: { title, description, eligible, contractAddress, url }, + airdrop: { name, description, eligible, contractAddress, url }, authenticated, }: CampaignCardProps) => { return (
-

{title}

+

{name}

{description}

{authenticated && contractAddress && ( diff --git a/airdrops/components/CampaignDetailContent.tsx b/airdrops/components/CampaignDetailContent.tsx index 00669181dc2..3c7b589d3b9 100644 --- a/airdrops/components/CampaignDetailContent.tsx +++ b/airdrops/components/CampaignDetailContent.tsx @@ -1,4 +1,5 @@ 'use client' +import { StandardMerkleTree } from '@openzeppelin/merkle-tree' import { ethers } from 'ethers' import { usePrivy, useWallets } from '@privy-io/react-auth' import { Container } from './layout/Container' @@ -11,6 +12,7 @@ import { isEligible } from '../src/utils/eligibility' import { AirdropData } from './Campaigns' import ReactMarkdown from 'react-markdown' import { terms } from '../src/utils/terms' +import { UPAirdrops } from '@unlock-protocol/contracts' interface CampaignDetailContentProps { airdrop: AirdropData @@ -18,6 +20,24 @@ interface CampaignDetailContentProps { const timestamp = new Date().getTime() +const getContract = async (address: string, network: number) => { + const provider = new ethers.JsonRpcProvider( + `https://rpc.unlock-protocol.com/${network}` + ) + return new ethers.Contract(address, UPAirdrops.abi, provider) +} + +const getProof = async (address: string, airdrop: AirdropData) => { + const request = await fetch(airdrop.recipientsFile) + const tree = StandardMerkleTree.load(await request.json()) + for (const [i, leaf] of tree.entries()) { + if (leaf[0].toLowerCase() === address.toLowerCase()) { + const proof = tree.getProof(i) + return { leaf, proof } + } + } +} + export default function CampaignDetailContent({ airdrop, }: CampaignDetailContentProps) { @@ -41,13 +61,17 @@ export default function CampaignDetailContent({ const ethersProvider = new ethers.BrowserProvider(provider) const signer = await ethersProvider.getSigner() - await wallets[0].switchChain(8453) + await wallets[0].switchChain(airdrop.chainId) + const contract = await getContract( + airdrop.contractAddress, + airdrop.chainId + ) const domain = { - name: 'Airdrops', // await airdrops.EIP712Name(), - version: '1', // await airdrops.EIP712Version(), - chainId: 8453, - verifyingContract: '0x4200000000000000000000000000000000000011', // replace me + name: await contract.EIP712Name(), + version: await contract.EIP712Version(), + chainId: airdrop.chainId, + verifyingContract: airdrop.contractAddress, } const types = { @@ -60,7 +84,7 @@ export default function CampaignDetailContent({ const value = { signer: signer.address, - campaignName: airdrop.title, + campaignName: airdrop.name, timestamp, } @@ -72,8 +96,30 @@ export default function CampaignDetailContent({ } const onClaim = async () => { - console.log('Claiming tokens for', airdrop.contractAddress) - console.log({ termsOfServiceSignature, timestamp }) + const provider = await wallets[0].getEthereumProvider() + const ethersProvider = new ethers.BrowserProvider(provider) + const signer = await ethersProvider.getSigner() + + const airdropContract = await getContract( + airdrop.contractAddress, + airdrop.chainId + ) + + // Get the proof! + const { proof } = await getProof(wallets[0].address, airdrop) + console.log(proof) + + const tx = await airdropContract + .connect(signer) + .claim( + airdrop.name, + timestamp, + wallets[0].address, + airdrop.eligible, + proof, + termsOfServiceSignature + ) + await tx.wait() } return ( @@ -86,7 +132,7 @@ export default function CampaignDetailContent({ {/* Full-width title and description */}
-

{airdrop.title}

+

{airdrop.name}

{airdrop.description}

diff --git a/airdrops/components/Campaigns.tsx b/airdrops/components/Campaigns.tsx index 26fa5b8dc1d..d2204af04ae 100644 --- a/airdrops/components/Campaigns.tsx +++ b/airdrops/components/Campaigns.tsx @@ -11,16 +11,18 @@ import { CampaignCard } from './CampaignCard' export interface AirdropData { id: string - title: string + name: string description: string contractAddress?: string - token: { + token?: { address: string symbol: string decimals: number } - recipientsFile: string + recipientsFile?: string eligible?: number + url?: string + chainId?: number } const CampaignsContent = () => { diff --git a/airdrops/package.json b/airdrops/package.json index 624de32a094..e0d068159a8 100644 --- a/airdrops/package.json +++ b/airdrops/package.json @@ -13,6 +13,7 @@ "@sentry/nextjs": "8.54.0", "@tanstack/react-query": "5.59.19", "@tw-classed/react": "1.7.0", + "@unlock-protocol/contracts": "workspace:*", "@unlock-protocol/core": "workspace:./packages/core", "@unlock-protocol/crypto-icon": "workspace:./packages/crypto-icon", "@unlock-protocol/eslint-config": "workspace:./packages/eslint-config", diff --git a/airdrops/src/airdrops.json b/airdrops/src/airdrops.json index a7ba5200149..317058814d9 100644 --- a/airdrops/src/airdrops.json +++ b/airdrops/src/airdrops.json @@ -1,13 +1,13 @@ [ { "id": "1", - "title": "UP Token Swap", + "name": "UP Token Swap", "description": "The Unlock DAO migration to Base is complete, and the UP token swap reward airdrop, totaling 1.061 million UP tokens, is now live for all eligible participants.", "url": "https://unlock-protocol.com/blog/up-token-swap-reward-airdrop-now-live-" }, { "id": "2", - "title": "Blastoff Airdrop", + "name": "Blastoff Airdrop", "description": "The Unlock Protocol Foundation is launching a next airdrop to Unlock Protocol community members, distributing over 7 million $UP tokens on Base to over 10,000 members of the community!", "contractAddress": "0x3b26D06Ea8252a73742d2125D1ACEb594ECEE5c6", "recipientsFile": "https://merkle-trees.unlock-protocol.com/0xe238effc14b43022c9ce132e22f0baa73cdd8696f4b435150a4c9341c83abfbf.json", @@ -15,11 +15,12 @@ "address": "0x3b26D06Ea8252a73742d2125D1ACEb594ECEE5c6", "symbol": "UP", "decimals": 18 - } + }, + "chainId": 8453 }, { "id": "3", - "title": "Trading Volume", + "name": "Trading Volume", "description": "More details to be announced soon!" } ] diff --git a/yarn.lock b/yarn.lock index d4160553b38..f721df853c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18622,6 +18622,7 @@ __metadata: "@tanstack/react-query": "npm:5.59.19" "@tw-classed/react": "npm:1.7.0" "@types/react": "npm:18.3.18" + "@unlock-protocol/contracts": "workspace:*" "@unlock-protocol/core": "workspace:./packages/core" "@unlock-protocol/crypto-icon": "workspace:./packages/crypto-icon" "@unlock-protocol/eslint-config": "workspace:./packages/eslint-config" @@ -18652,7 +18653,7 @@ __metadata: languageName: unknown linkType: soft -"@unlock-protocol/contracts@workspace:./packages/contracts, @unlock-protocol/contracts@workspace:^, @unlock-protocol/contracts@workspace:packages/contracts": +"@unlock-protocol/contracts@workspace:*, @unlock-protocol/contracts@workspace:./packages/contracts, @unlock-protocol/contracts@workspace:^, @unlock-protocol/contracts@workspace:packages/contracts": version: 0.0.0-use.local resolution: "@unlock-protocol/contracts@workspace:packages/contracts" dependencies: From 1f36534f0e7b04d36e2532a9604604c92fe5de00 Mon Sep 17 00:00:00 2001 From: Julien Genestoux Date: Wed, 26 Feb 2025 16:25:59 -0500 Subject: [PATCH 4/5] typescript --- airdrops/components/CampaignDetailContent.tsx | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/airdrops/components/CampaignDetailContent.tsx b/airdrops/components/CampaignDetailContent.tsx index 3c7b589d3b9..c5db05880ee 100644 --- a/airdrops/components/CampaignDetailContent.tsx +++ b/airdrops/components/CampaignDetailContent.tsx @@ -20,11 +20,17 @@ interface CampaignDetailContentProps { const timestamp = new Date().getTime() -const getContract = async (address: string, network: number) => { - const provider = new ethers.JsonRpcProvider( - `https://rpc.unlock-protocol.com/${network}` +const getContract = async ( + address: string, + network: number, + provider?: ethers.Provider +) => { + return new ethers.Contract( + address, + UPAirdrops.abi, + provider || + new ethers.JsonRpcProvider(`https://rpc.unlock-protocol.com/${network}`) ) - return new ethers.Contract(address, UPAirdrops.abi, provider) } const getProof = async (address: string, airdrop: AirdropData) => { @@ -36,6 +42,7 @@ const getProof = async (address: string, airdrop: AirdropData) => { return { leaf, proof } } } + return { leaf: null, proof: null } } export default function CampaignDetailContent({ @@ -98,27 +105,25 @@ export default function CampaignDetailContent({ const onClaim = async () => { const provider = await wallets[0].getEthereumProvider() const ethersProvider = new ethers.BrowserProvider(provider) - const signer = await ethersProvider.getSigner() const airdropContract = await getContract( airdrop.contractAddress, - airdrop.chainId + airdrop.chainId, + ethersProvider ) // Get the proof! const { proof } = await getProof(wallets[0].address, airdrop) console.log(proof) - const tx = await airdropContract - .connect(signer) - .claim( - airdrop.name, - timestamp, - wallets[0].address, - airdrop.eligible, - proof, - termsOfServiceSignature - ) + const tx = await airdropContract.claim( + airdrop.name, + timestamp, + wallets[0].address, + airdrop.eligible, + proof, + termsOfServiceSignature + ) await tx.wait() } From 420bb6991d2b236037678201d5e337d356d85c9c Mon Sep 17 00:00:00 2001 From: Julien Genestoux Date: Wed, 26 Feb 2025 16:29:54 -0500 Subject: [PATCH 5/5] fixed typescript --- airdrops/components/CampaignDetailContent.tsx | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/airdrops/components/CampaignDetailContent.tsx b/airdrops/components/CampaignDetailContent.tsx index c5db05880ee..c2e79755659 100644 --- a/airdrops/components/CampaignDetailContent.tsx +++ b/airdrops/components/CampaignDetailContent.tsx @@ -20,17 +20,11 @@ interface CampaignDetailContentProps { const timestamp = new Date().getTime() -const getContract = async ( - address: string, - network: number, - provider?: ethers.Provider -) => { - return new ethers.Contract( - address, - UPAirdrops.abi, - provider || - new ethers.JsonRpcProvider(`https://rpc.unlock-protocol.com/${network}`) +const getContract = async (address: string, network: number) => { + const provider = new ethers.JsonRpcProvider( + `https://rpc.unlock-protocol.com/${network}` ) + return new ethers.Contract(address, UPAirdrops.abi, provider) } const getProof = async (address: string, airdrop: AirdropData) => { @@ -105,25 +99,28 @@ export default function CampaignDetailContent({ const onClaim = async () => { const provider = await wallets[0].getEthereumProvider() const ethersProvider = new ethers.BrowserProvider(provider) + const signer = await ethersProvider.getSigner() const airdropContract = await getContract( airdrop.contractAddress, - airdrop.chainId, - ethersProvider + airdrop.chainId ) // Get the proof! const { proof } = await getProof(wallets[0].address, airdrop) console.log(proof) - const tx = await airdropContract.claim( - airdrop.name, - timestamp, - wallets[0].address, - airdrop.eligible, - proof, - termsOfServiceSignature - ) + const tx = await airdropContract + .connect(signer) + // @ts-expect-error Property 'claim' does not exist on type 'BaseContract'.ts(2339) + .claim( + airdrop.name, + timestamp, + wallets[0].address, + airdrop.eligible, + proof, + termsOfServiceSignature + ) await tx.wait() }