├── .env.example
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.js
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── api
│ └── burn.ts
└── index.tsx
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── bonklogo.webp
├── burn.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── github.svg
├── next.svg
├── site.webmanifest
├── thirteen.svg
└── vercel.svg
├── sandstorm.md
├── styles
├── Home.module.css
└── globals.css
├── tsconfig.json
└── utils
├── api
├── crossmint.ts
└── discord.ts
├── constants.ts
├── samples
├── my_cm.json
├── samplePOST.md
├── sample_cm.json
└── sample_helius.json
├── solana
├── fetchTokens.ts
└── phantom.ts
└── utils.ts
/.env.example:
--------------------------------------------------------------------------------
1 | AUTH_CODE=YOUR_SECRET_HERE
2 | CROSS_MINT_SECRET=
3 | CROSS_MINT_PROJECT=
4 | MIN_BURN_AMT = 1000000
5 | TOKEN_MINT = 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263'
6 | TOKEN_NAME = BONK
7 | DISCORD_API_TOKEN = YOUR_BOT
8 | DISCORD_CHANNEL_ID = YOUR_CHANNEL
9 | NEXT_PUBLIC_RPC = YOUR_PRIVATE_RPC_URL
10 | NEXT_PUBLIC_MIN_BURN_AMT = 1
11 | NEXT_PUBLIC_TOKEN_MINT = DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263
12 | NEXT_PUBLIC_TOKEN_NAME = BONK
13 | NEXT_PUBLIC_NUM_DECIMALS = 5
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | .env
39 | NOTES.md
40 | yarn.lock
41 | nftart*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Submitted to the 2023 Sandstorm Hackathon. Application available: `sandstorm.md`
2 |
3 | # proof-of-x
4 | Mint and Drop an NFT after Verifying a Specific Solana Event has occurred
5 |
6 | This is a serverless function that handles a `POST` request from Helius on Vercel using Next.js. The function:
7 |
8 | - Validates and authorizes the `POST` request
9 | - Verifies that the type of transaction received matches what was expected (in this case 'BURN').
10 | - It extracts the `tokenTransfer` array from the body, and verifies that the tokens burned is greater than the `MIN_BURN` constant.
11 | - If the amount of tokens burned is greater than the `MIN_BURN` constant, we send a call to CrossMint to mint an NFT to the burner or `pyro`.
12 |
13 | >*Note, the front end here is really is just a tool to let ppl easily burn tokens. There's no link between the FE/BE. So you're welcome to play around with the FE, but the fun stuff is in `pages/api/burn.ts`.*
14 |
15 | ## Getting Started
16 |
17 | - Clone from GH.
18 | - Rename `.env.example` to `.env` and update the variables. \
19 | - `AUTH_CODE` is self generated (i like solana keygen-grind to find something secure)
20 | - `CROSS_MINT` envs require a dev account from [CrossMint](https://staging.crossmint.com/)
21 | - `DISCORD` keys are available via the [Developer Portal](https://discord.com/developers/docs/intro)
22 | - * NOTE: You can disable DISCORD BOT by setting `const NOTIFY_DISCORD = false` in `pages/api/burn.ts`, in which case you shouldn't need these keys
23 | - You'll need a [Helius WebHook](https://dev.helius.xyz/webhooks/my). At present this repo is only really indexed for the `Enhanced Transaction` type `BURN`. The verification component is pretty modular and could easily be replaced with some other Tx Type and different success criteria. You'll need to wait to deploy the webhook until after you've launched your server.
24 | - Modify NFT rewards/metadata to your liking.
25 |
26 | You should be able to test things out. Install dependencies and launch the app:
27 |
28 | ```sh
29 | yarn
30 | ```
31 |
32 | ```sh
33 | yarn dev
34 | ```
35 |
36 | - Out the gate, you should be able to use `utils/samples/samplePOST.md` to run a `curl` command in a seperate terminal--this should effectively simulate recieving a `POST` request from the WebHook. Just make sure you're sending the request to the right path (in my case, `http://localhost:3000/api/burn`)
37 | You should see something like this in your server terminal:
38 |
39 | ```sh
40 | Requesting NFT Mint:
41 | - Pyro: Cw9P...4J1E
42 | - Burn: 10,055,679 $BONK
43 | - TxId: JWSn...JoDR
44 | - CMID: w44177a9-02e9-4d04-a5ce-de7d5739d5bx
45 | ✅ Mint: arGbzUJQ1vTmjqPdDTqXsuqPxLQb1MGkUX5qTXt483Kx
46 | ✅ DCRD: 🤖 Connection Established
47 | ✅ DCRD: Message sent to server
48 | ✅ DCRD: 🤖 Connection destroyed
49 | ```
50 |
51 | To get the feed from Helius, you need to push your component to a serverless function hosting provider. I used [Vercel](https://vercel.com). I note some issues/limitations below, especially on their free plan.
52 |
53 |
54 | ### Potential Limitations / Known Issues
55 |
56 | At present (Jan 21, 2023):
57 | - Webhooks:
58 | - BURN Feed does not include inner tx Burns (e.g., burns associated with a Candy Machine mint)
59 | - BURN Feed does not include BurnChecked tx's
60 | - Vercel:
61 | - Base plan is limited to 10s executions (which is only enough time to send a CM request)
62 | - Pro plan is limited to 60s which is sufficient MOST of the time but I have seen some executions time out
63 | - Future phases will have to explore alternatives
64 | - Note: The Serverless Function logs will automatically be cut-off if the total size reaches over 4KB. This will require a log drain for future use
65 | - CM:
66 | - I don't believe metadata currently uses [Metaplex's latest standard](https://docs.metaplex.com/programs/token-metadata/token-standard). I think the lack of `properties.files` for the img file causes the NFT to not render on all platforms (e.g., Solana Explorer)
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "proof-of-x",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@next/font": "13.1.2",
13 | "@solana/spl-token": "^0.3.7",
14 | "@solana/web3.js": "^1.73.0",
15 | "@types/node": "18.11.18",
16 | "@types/react": "18.0.27",
17 | "@types/react-dom": "18.0.10",
18 | "discord.js": "^14.7.1",
19 | "eslint": "8.32.0",
20 | "eslint-config-next": "13.1.2",
21 | "next": "13.1.2",
22 | "node-fetch": "^3.3.0",
23 | "react": "18.2.0",
24 | "react-dom": "18.2.0",
25 | "typescript": "4.9.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css'
2 | import type { AppProps } from 'next/app'
3 |
4 | export default function App({ Component, pageProps }: AppProps) {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
13 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/pages/api/burn.ts:
--------------------------------------------------------------------------------
1 | import { cmMintNft } from '@/utils/api/crossmint';
2 | import { sendDiscordMsg } from '@/utils/api/discord';
3 | import { cleanDate, generateExplorerUrl, shortHash } from '@/utils/utils';
4 | import { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | const AUTH_CODE = process.env.AUTH_CODE;
7 | const MIN_BURN = Number(process.env.MIN_BURN_AMT);
8 | const TOKEN_MINT = process.env.TOKEN_MINT;
9 | const DISCORD_API_TOKEN = process.env.DISCORD_API_TOKEN;
10 | const CHANNEL_ID = process.env.DISCORD_CHANNEL_ID;
11 | const TOKEN_NAME = process.env.TOKEN_NAME ?? 'TOKEN';
12 | const NOTIFY_DISCORD = true; // set false if no Discord
13 | const envVars = [AUTH_CODE, MIN_BURN, TOKEN_MINT, process.env.CROSS_MINT_SECRET, process.env.CROSS_MINT_PROJECT];
14 |
15 | interface TokenTransfer {
16 | fromAccount: string,
17 | fromUserAccount: string,
18 | mint: string,
19 | toTokenAccount: string,
20 | toUserAccount: string,
21 | tokenAmount: number,
22 | tokenStandard: string
23 | }
24 |
25 | export default async function handler(
26 | request: NextApiRequest,
27 | response: NextApiResponse,
28 | ) {
29 | const { body } = request;
30 | // CHECK ENV VAR SET
31 | for (const env of envVars) {
32 | if (!env) { return response.status(500).json({ error: 'Missing environment variable' }) }
33 | };
34 | // STEP 1 AUTHORIZE POST
35 | if (request.method !== 'POST') {
36 | return response.status(405).json({ error: 'Method Not Allowed.' });
37 | }
38 | if (!request.headers.authorization) {
39 | return response.status(400).json({ error: 'No credentials sent.' });
40 | }
41 | if (request.headers.authorization !== AUTH_CODE) {
42 | return response.status(403).json({ error: 'Invalid authorization.' });
43 | }
44 |
45 | // STEP 2 VERIFY BURN
46 | const data = body[0];
47 | if (!data || !body || !body.length) {
48 | return response.status(400).json('No data in body.');
49 | }
50 | if (!data.type || data.type !== 'BURN' || !data.tokenTransfers) {
51 | return response.status(400).json('Data wrong type.');
52 | }
53 | const tokenTransfers: TokenTransfer[] = data.tokenTransfers;
54 | // Find the tx for specified mint and verify reciever is null (Helius shows burns and transfers to nobody)
55 | let burnTx = tokenTransfers.find(transfer => {
56 | return (
57 | (transfer.mint == TOKEN_MINT) &&
58 | !transfer.toTokenAccount &&
59 | !transfer.toUserAccount
60 | )
61 | })
62 | if (!burnTx) {
63 | console.log('No burn transaction found.');
64 | return response.status(400).json('No burn tranfer found');
65 | }
66 |
67 | const pyro = burnTx.fromUserAccount; // use the owner of the burned tokens
68 | const burnAmount = burnTx.tokenAmount.toLocaleString(undefined, { maximumFractionDigits: 0 });
69 | const burnQty = burnTx.tokenAmount; // use for tiered rewards
70 | const { signature, timestamp } = data;
71 |
72 | if (burnTx.tokenAmount < MIN_BURN) {
73 | console.log(`${burnAmount} tokens burned is less than threshold.`);
74 | return response.status(200).json('Smol burn');
75 | }
76 |
77 | console.log('Requesting NFT Mint:');
78 | console.log(` - Pyro: ${shortHash(pyro)}`);
79 | console.log(` - Burn: ${burnAmount} $${TOKEN_NAME}`);
80 | console.log(` - TxId: ${shortHash(signature)}`);
81 |
82 | // Step 3 - Mint NFT
83 | try {
84 | let newMint = await cmMintNft(pyro, burnAmount, timestamp, TOKEN_NAME, signature, burnQty);
85 | if (!newMint || !newMint.id) { return response.status(204).json('No response from CM'); };
86 | if (!newMint.details) { console.log(`New mint not found for ${newMint.id}.`); return response.status(202).json('Mint status unknown'); }
87 | console.log(` ✅ Mint: ${newMint.details.onChain.mintHash}`);
88 | if (NOTIFY_DISCORD && CHANNEL_ID && DISCORD_API_TOKEN) {
89 | await sendDiscordMsg(
90 | `${shortHash(pyro)} burned ${burnAmount} $${TOKEN_NAME} and got this NFT: ${shortHash(newMint.details.onChain.mintHash)} <${generateExplorerUrl('', 'devnet', newMint.details.onChain.mintHash)}>`,
91 | CHANNEL_ID,
92 | DISCORD_API_TOKEN
93 | );
94 | }
95 | return response.status(200).json('🔥🔥🔥');
96 | }
97 | catch {
98 | return response.status(204).end();
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import Image from 'next/image'
3 | import { Inter } from '@next/font/google'
4 | import styles from '@/styles/Home.module.css'
5 | import usePhantom from '@/utils/solana/phantom'
6 | import { generateExplorerUrl, shortHash } from '@/utils/utils'
7 | import { useCallback, useState } from 'react'
8 | import { MIN_BURN_AMT, NUM_DECIMALS, TOKEN_MINT } from '@/utils/constants'
9 | import { createBurnCheckedInstruction, createBurnInstruction } from '@solana/spl-token'
10 | import { PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js'
11 |
12 | const inter = Inter({ subsets: ['latin'] })
13 |
14 | export default function Home() {
15 | const [loading, setLoading] = useState(false);
16 | const [complete, setComplete] = useState(false);
17 | const [resultMsg, setResultMsg] = useState('');
18 | const [formText, setFormText] = useState('1,000,000');
19 | const [burnAmt, setBurnAmt] = useState(1000000);
20 | const [txid, setTxid] = useState('');
21 | const [notice, setNotice] = useState(<>>);
22 | const { provider, connection, tokenBalance, pubKey, connect, disconnect, isConnected, ata } = usePhantom();
23 | const handleClick = useCallback(() => {
24 | return;
25 | if (!isConnected) { connect() }
26 | //else { disconnect() }
27 | }, [isConnected, connect])
28 |
29 | const handleTyping = useCallback((e:React.FormEvent)=>{
30 | e.preventDefault();
31 | const addCommas = (num:string) => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
32 | const removeNonNumeric = (num: string) => num.toString().replace(/[^0-9]/g, "");
33 | let typedValue = e.currentTarget.value;
34 | setBurnAmt(parseInt(removeNonNumeric(typedValue)) ?? 1000000);
35 | console.log('new burn amt', parseInt(removeNonNumeric(typedValue)) ?? 1000000);
36 | setFormText(addCommas(removeNonNumeric(typedValue)));
37 | },[])
38 | const handleBurn = useCallback(async () => {
39 | if (!provider) return;
40 | if (!pubKey || !ata) return;
41 | if (!tokenBalance) return;
42 | if (tokenBalance < MIN_BURN_AMT) return;
43 | setLoading(true);
44 | let burnIx: TransactionInstruction = createBurnInstruction(
45 | ata,
46 | new PublicKey(TOKEN_MINT),
47 | pubKey,
48 | burnAmt * (10 ** NUM_DECIMALS)
49 | )
50 | // At the momeont (2023/1/21), Hook does not appear to include BurnChecked
51 | let burnIxChecked: TransactionInstruction = createBurnCheckedInstruction(
52 | ata,
53 | new PublicKey(TOKEN_MINT),
54 | pubKey,
55 | MIN_BURN_AMT * (10 ** NUM_DECIMALS),
56 | NUM_DECIMALS
57 | );
58 | let burnTx = new Transaction().add(burnIx);
59 | const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
60 | if (!blockhash || !lastValidBlockHeight) return;
61 | burnTx.lastValidBlockHeight = lastValidBlockHeight;
62 | burnTx.recentBlockhash = blockhash;
63 | burnTx.feePayer = pubKey;
64 |
65 | try {
66 | const { signature } = await provider.signAndSendTransaction(burnTx);
67 | const confirmation = await connection.confirmTransaction({ signature, lastValidBlockHeight, blockhash }, 'confirmed');
68 | if (confirmation.value.err) {
69 | throw Error("unable to confirm transaciton")
70 | }
71 | setResultMsg('🔥 BURN SUCCESS 🔥');
72 | setTxid(signature);
73 | setNotice(<>Proof of X is now listening for this transaction on chain!
74 | If NFTs are still available, you should recieve an airdrop shortly!
75 | Keep an eye on your wallet...>
76 | )
77 | }
78 | catch {
79 | setResultMsg('💥 BONK! 💥');
80 | setNotice(<>Something went wrong with your transaction.
81 | Take a breather and try again!>
82 | )
83 | }
84 | finally {
85 | setComplete(true);
86 | setLoading(false);
87 | }
88 |
89 |
90 | }, [pubKey, tokenBalance, ata, connection, provider])
91 | const handleReset = useCallback(() => {
92 | setLoading(false);
93 | setComplete(false);
94 | setResultMsg('');
95 | setTxid('');
96 | setNotice(<>>);
97 | setFormText('1,000,000');
98 | setBurnAmt(1000000);
99 | }, [])
100 | return (
101 | <>
102 |
103 | Proof of X
104 |
105 |
106 |
107 |
108 |
109 | {/* MODAL TEXT */}
110 |
111 |
112 | {/* CLOSE BUTTON */}
113 |
×
114 |
115 |
{resultMsg}
116 | {notice}
117 | {txid && <>Transaction ID:
122 | {shortHash(txid)}
123 | >}
124 |
125 |
126 | {/* BODY */}
127 |
128 |
129 | Proof of X Real-time rewards for on-chain events
130 |
131 |
147 |
148 |
149 |
150 |
155 |
163 |
164 |
165 |
{loading && 'BURNING'}
166 |
167 |
168 | {!isConnected ?
{if (!isConnected) { return; connect() }}}
171 | >
172 |
173 | Phase 1 Ended 🔥🔥🔥
174 |
175 |
176 | Check back for updates
177 |
178 |
179 |
180 | 🔴 Not Connected connect disabled
181 |
182 | :
183 |
187 |
188 | {((tokenBalance ?? 0) >= MIN_BURN_AMT) ? '🔥Click Dog to Burn🔥' : 'MORE BONK NEEDED'}
189 |
190 |
191 | Burn 1M+ BONK ->Get NFTWARNING: Burn is irreversible
192 |
193 |
194 | 🟢 Connected to {shortHash(pubKey?.toString())}
195 |
196 |
197 |
198 |
199 |
200 |
201 |
}
202 |
203 |
204 |
241 |
242 | >
243 | )
244 | }
245 |
246 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amilz/proof-of-x/6a50c567b529057dc64f2d28bda817746248cfd4/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amilz/proof-of-x/6a50c567b529057dc64f2d28bda817746248cfd4/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amilz/proof-of-x/6a50c567b529057dc64f2d28bda817746248cfd4/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/bonklogo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amilz/proof-of-x/6a50c567b529057dc64f2d28bda817746248cfd4/public/bonklogo.webp
--------------------------------------------------------------------------------
/public/burn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amilz/proof-of-x/6a50c567b529057dc64f2d28bda817746248cfd4/public/burn.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amilz/proof-of-x/6a50c567b529057dc64f2d28bda817746248cfd4/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amilz/proof-of-x/6a50c567b529057dc64f2d28bda817746248cfd4/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amilz/proof-of-x/6a50c567b529057dc64f2d28bda817746248cfd4/public/favicon.ico
--------------------------------------------------------------------------------
/public/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sandstorm.md:
--------------------------------------------------------------------------------
1 | # Proof of X
2 |
3 | ### Team Details:
4 | @AMilz#7564: Role: Fullstack, Wallet address: `As63vJGYr8q3rZ2CrazfwMMNKHvfaosvaCmyRpAUz6KQ`
5 | *Thanks to @Nobody#6477 for code review and feedback!*
6 |
7 | ### Simple Description:
8 | **Proof of X** *real-time rewards for on-chain events*.
9 | Proof of X is a platform that provides both real-time verification of, and rewards for, a specified on-chain task. Using Webhooks, we can monitor the Solana blockchain for specific activities. When triggered, our backend mints an NFT to the user memorializing the act and rewarding the user for that task.
10 |
11 | We are launching "Proof of Burn - BONK" for our demonstration in this hackathon. We have a live demo that will mint anybody a Proof of Burn NFT for burning 1M $BONK. Though this process is triggered from any valid on-chain event, we have created a simple [front end for users to burn 1M $BONK](https://sandstorm.amilz.com/).
12 |
13 | ### Tracks Chosen & Why:
14 | Track 1 - NFTs: Proof of X utilizes NFTs to provide proof of authenticity of specific on-chain activities. Though the demonstration application is fun and in the spirit of BONK, Proof of X has value beyond the fun. This platform will provide real-world utility for many business applications (e.g., proof of purchase/receipts, marketing, loyatly rewards programs, proof of attendance, etc.). Proof of X uses Helius Webhooks for tracking activity and Crossmint minting API to create and deliver NFTs.
15 | Track 2 - Bonking with Bonk!: We give anybody who burns > 1M $BONK a free NFT. Automatically. BONK!
16 | Track 3 - Automation: Proof of X automates the verification of on-chain activities and the delivery of NFTs as rewards or receipts for such activities. Proof of X utilizes Helius Webhooks and is evaluating opportunities to leverage Clockwork for future offerings.
17 |
18 | ### Link
19 | Pitch Deck: https://docs.google.com/presentation/d/1KBQFO9JGEOoRuq47rTfrGOGOH-bnBCx-064QIhGlMjI/
20 | Live Demo: Go burn 1M $BONK, and you'll get an NFT. That simple! I have created a burn page to do it easily (this front end is separate from the backend that automates your burn verification and NFT minting). [Burn 1M $BONK](https://sandstorm.amilz.com/).
21 | *NOTE! At present, we are limited to top level Burn instructions (not innner instructions) and limited to Burn, not BurnChecked. If you don't know what that means, I'd recommend using the demo link for your burn (or DM me and I can help you figure it out).
22 | GitHub Link: https://github.com/amilz/proof-of-x
23 | Is your GitHub open sourced: Yes
24 | Twitter Thread: (soon)
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 3rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .title {
22 | font-size: 2.6em;
23 | font-weight: bold;
24 | pointer-events:none;
25 | }
26 | .subhead {
27 | font-size: 1.2em;
28 | pointer-events:none;
29 | }
30 | @media (max-width: 768px) {
31 | .title {
32 | font-size: 2.4em;
33 | }
34 | .subhead {
35 | font-size: 1em;
36 | }
37 | }
38 |
39 | .description a {
40 | display: flex;
41 | justify-content: center;
42 | align-items: center;
43 | gap: 0.5rem;
44 | }
45 |
46 | .description p {
47 | position: relative;
48 | margin: 0;
49 | padding: 1rem;
50 | background-color: rgba(var(--callout-rgb), 0.5);
51 | border: none;
52 | border-radius: var(--border-radius);
53 | }
54 |
55 | .code {
56 | font-weight: 700;
57 | font-family: var(--font-mono);
58 | }
59 |
60 | .grid {
61 | display: grid;
62 | grid-template-columns: repeat(3, minmax(33%, auto));
63 | width: var(--max-width);
64 | max-width: 100%;
65 | }
66 |
67 | .card {
68 | padding: 1rem 1.2rem;
69 | border-radius: var(--border-radius);
70 | background: rgba(var(--card-rgb), 0);
71 | border: 1px solid rgba(var(--card-border-rgb), 0);
72 | transition: background 200ms, border 200ms;
73 | cursor: default;
74 | margin: auto;
75 | }
76 |
77 | .card span {
78 | display: inline-block;
79 | transition: transform 200ms;
80 | }
81 |
82 | .card h2 {
83 | font-weight: 600;
84 | margin-bottom: 0.7rem;
85 | text-align: center;
86 | }
87 |
88 | .card p {
89 | margin: 0;
90 | opacity: 0.6;
91 | font-size: 0.9rem;
92 | line-height: 1.5;
93 | max-width: 30ch;
94 | text-align: center;
95 | }
96 |
97 | .center {
98 | display: flex;
99 | justify-content: center;
100 | align-items: center;
101 | position: relative;
102 | padding: 2.5rem 0;
103 | }
104 |
105 | .center::before {
106 | background: var(--secondary-glow);
107 | border-radius: 50%;
108 | width: 480px;
109 | height: 360px;
110 | margin-left: -400px;
111 | }
112 |
113 | .center::after {
114 | background: var(--primary-glow);
115 | width: 240px;
116 | height: 180px;
117 | z-index: -1;
118 | }
119 |
120 | .center::before,
121 | .center::after {
122 | content: '';
123 | left: 50%;
124 | position: absolute;
125 | filter: blur(45px);
126 | transform: translateZ(0);
127 | }
128 |
129 | .logo,
130 | .thirteen {
131 | position: relative;
132 | -moz-box-shadow: 1px 2px 3px rgba(0, 0, 0, .5);
133 | -webkit-box-shadow: 1px 2px 3px rgba(0, 0, 0, .5);
134 | box-shadow: 1px 2px 3px rgba(0, 0, 0, .5);
135 | border-radius: 50%;
136 | }
137 |
138 | .thirteen {
139 | display: flex;
140 | justify-content: center;
141 | align-items: center;
142 | width: 75px;
143 | height: 75px;
144 | padding: 25px 10px;
145 | margin-left: 16px;
146 | transform: translateZ(0);
147 | border-radius: var(--border-radius);
148 | overflow: hidden;
149 | box-shadow: 0px 2px 8px -1px #0000001a;
150 | }
151 |
152 | .thirteen::before,
153 | .thirteen::after {
154 | content: '';
155 | position: absolute;
156 | z-index: -1;
157 | }
158 |
159 | /* Conic Gradient Animation */
160 | .thirteen::before {
161 | animation: 6s rotate linear infinite;
162 | width: 200%;
163 | height: 200%;
164 | background: var(--tile-border);
165 | }
166 |
167 | /* Inner Square */
168 | .thirteen::after {
169 | inset: 0;
170 | padding: 1px;
171 | border-radius: var(--border-radius);
172 | background: linear-gradient(to bottom right,
173 | rgba(var(--tile-start-rgb), 1),
174 | rgba(var(--tile-end-rgb), 1));
175 | background-clip: content-box;
176 | }
177 |
178 | /* Enable hover only on non-touch devices */
179 | @media (hover: hover) and (pointer: fine) {
180 | .card:hover {
181 | background: rgba(var(--card-rgb), 0.1);
182 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
183 | }
184 |
185 | .card:hover span {
186 | transform: translateX(4px);
187 | }
188 | }
189 |
190 | @media (prefers-reduced-motion) {
191 | .thirteen::before {
192 | animation: none;
193 | }
194 |
195 | .card:hover span {
196 | transform: none;
197 | }
198 | }
199 |
200 | /* Mobile */
201 | @media (max-width: 700px) {
202 | .content {
203 | padding: 4rem;
204 | }
205 |
206 | .grid {
207 | grid-template-columns: 1fr;
208 | margin-bottom: 120px;
209 | max-width: 320px;
210 | text-align: center;
211 | }
212 |
213 | .card {
214 | padding: 1rem 2.5rem;
215 | }
216 |
217 | .card h2 {
218 | margin-bottom: 0.5rem;
219 | }
220 |
221 | .center {
222 | padding: 8rem 0 6rem;
223 | }
224 |
225 | .center::before {
226 | transform: none;
227 | height: 300px;
228 | }
229 |
230 | .description {
231 | font-size: 0.8rem;
232 | }
233 |
234 | .description a {
235 | padding: 1rem;
236 | }
237 |
238 | .description p,
239 | .description div {
240 | display: flex;
241 | justify-content: center;
242 | position: fixed;
243 | width: 100%;
244 | flex-wrap: wrap;
245 | }
246 |
247 | .description p {
248 | align-items: center;
249 | inset: 0 0 auto;
250 | padding: 2rem 1rem 1.4rem;
251 | border-radius: 0;
252 | border: none;
253 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
254 | background: linear-gradient(to bottom,
255 | rgba(var(--background-start-rgb), 1),
256 | rgba(var(--callout-rgb), 0.5));
257 | background-clip: padding-box;
258 | backdrop-filter: blur(24px);
259 | }
260 |
261 | .description div {
262 | align-items: flex-end;
263 | pointer-events: auto;
264 | inset: auto 0 0;
265 | padding: 2rem;
266 | height: 200px;
267 | background: linear-gradient(to bottom,
268 | transparent 0%,
269 | rgb(var(--background-end-rgb)) 40%);
270 | z-index: 20;
271 | }
272 | }
273 |
274 | /* Tablet and Smaller Desktop */
275 |
276 |
277 |
278 | @keyframes rotate {
279 | from {
280 | transform: rotate(360deg);
281 | }
282 |
283 | to {
284 | transform: rotate(0deg);
285 | }
286 | }
287 |
288 | .walletDetails {
289 | font-size: smaller;
290 | }
291 |
292 | @media (max-width: 768px) {
293 | .center {
294 | flex-direction: column;
295 | padding: 0;
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
7 |
8 | --foreground-rgb: 255, 255, 255;
9 | --background-start-rgb: 0, 0, 0;
10 | --background-end-rgb: 0, 0, 0;
11 |
12 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
13 | --secondary-glow: linear-gradient(to bottom right,
14 | rgba(1, 65, 255, 0),
15 | rgba(1, 65, 255, 0),
16 | rgba(1, 65, 255, 0.3));
17 |
18 | --tile-start-rgb: 2, 13, 46;
19 | --tile-end-rgb: 2, 5, 19;
20 | --tile-border: conic-gradient(#ffffff80,
21 | #ffffff40,
22 | #ffffff30,
23 | #ffffff20,
24 | #ffffff10,
25 | #ffffff10,
26 | #ffffff80);
27 |
28 | --callout-rgb: 20, 20, 20;
29 | --callout-border-rgb: 108, 108, 108;
30 | --card-rgb: 100, 100, 100;
31 | --card-border-rgb: 200, 200, 200;
32 | }
33 |
34 | @media (prefers-color-scheme: dark) {
35 | :root {
36 | --foreground-rgb: 255, 255, 255;
37 | --background-start-rgb: 0, 0, 0;
38 | --background-end-rgb: 0, 0, 0;
39 |
40 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
41 | --secondary-glow: linear-gradient(to bottom right,
42 | rgba(1, 65, 255, 0),
43 | rgba(1, 65, 255, 0),
44 | rgba(1, 65, 255, 0.3));
45 |
46 | --tile-start-rgb: 2, 13, 46;
47 | --tile-end-rgb: 2, 5, 19;
48 | --tile-border: conic-gradient(#ffffff80,
49 | #ffffff40,
50 | #ffffff30,
51 | #ffffff20,
52 | #ffffff10,
53 | #ffffff10,
54 | #ffffff80);
55 |
56 | --callout-rgb: 20, 20, 20;
57 | --callout-border-rgb: 108, 108, 108;
58 | --card-rgb: 100, 100, 100;
59 | --card-border-rgb: 200, 200, 200;
60 | }
61 | }
62 |
63 | @media (prefers-color-scheme: light) {
64 | :root {
65 | --foreground-rgb: 255, 255, 255;
66 | --background-start-rgb: 0, 0, 0;
67 | --background-end-rgb: 0, 0, 0;
68 |
69 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
70 | --secondary-glow: linear-gradient(to bottom right,
71 | rgba(1, 65, 255, 0),
72 | rgba(1, 65, 255, 0),
73 | rgba(1, 65, 255, 0.3));
74 |
75 | --tile-start-rgb: 2, 13, 46;
76 | --tile-end-rgb: 2, 5, 19;
77 | --tile-border: conic-gradient(#ffffff80,
78 | #ffffff40,
79 | #ffffff30,
80 | #ffffff20,
81 | #ffffff10,
82 | #ffffff10,
83 | #ffffff80);
84 |
85 | --callout-rgb: 20, 20, 20;
86 | --callout-border-rgb: 108, 108, 108;
87 | --card-rgb: 100, 100, 100;
88 | --card-border-rgb: 200, 200, 200;
89 | }
90 | }
91 |
92 | * {
93 | box-sizing: border-box;
94 | padding: 0;
95 | margin: 0;
96 | }
97 |
98 | html,
99 | body {
100 | max-width: 100vw;
101 | overflow-x: hidden;
102 | }
103 |
104 | body {
105 | color: rgb(var(--foreground-rgb));
106 | background: linear-gradient(to bottom,
107 | transparent,
108 | rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
109 | }
110 |
111 | a {
112 | color: inherit;
113 | text-decoration: none;
114 | }
115 |
116 | @media (prefers-color-scheme: dark) {
117 | html {
118 | color-scheme: dark;
119 | }
120 | }
121 |
122 | @media (prefers-color-scheme: light) {
123 | html {
124 | color-scheme: dark;
125 | }
126 | }
127 |
128 | button {
129 | cursor: pointer !important;
130 | }
131 |
132 | .dog-button {
133 | background: none;
134 | border: none;
135 | }
136 |
137 | .dog-button:hover {
138 | transform: scale(1.05) translate(0, 0);
139 | }
140 |
141 | .loading {
142 | background: none;
143 | border: none;
144 | visibility: hidden;
145 | opacity: 0;
146 | transition: visibility 0s 10s, opacity 10s linear;
147 | }
148 |
149 | .modal {
150 | display: block;
151 | /* Hidden by default */
152 | position: fixed;
153 | /* Stay in place */
154 | z-index: 10;
155 | /* Sit on top */
156 | left: 0;
157 | top: 0;
158 | width: 100%;
159 | /* Full width */
160 | height: 100%;
161 | /* Full height */
162 | overflow: auto;
163 | /* Enable scroll if needed */
164 | background-color: rgb(0, 0, 0);
165 | /* Fallback color */
166 | background-color: rgba(0, 0, 0, 0.4);
167 | /* Black w/ opacity */
168 | }
169 |
170 | .modal-content {
171 | background-color: #d2d2d2ce;
172 | margin: 12% auto;
173 | /* 15% from the top and centered */
174 | border: 1px solid #888;
175 | color: black;
176 | width: 40%;
177 | /* Could be more or less, depending on screen size */
178 | min-height: 40%;
179 | border-radius: 10px;
180 | font-family: var(--font-mono);
181 | padding: 50px;
182 | }
183 |
184 | /* Tablet and Smaller Desktop */
185 | @media (max-width: 777px) {
186 | .modal-content {
187 | width: 90%;
188 | height: 50%;
189 | padding: 20px;
190 | margin: 25% auto;
191 | }
192 | }
193 |
194 | /* The Close Button */
195 | .close {
196 | color: rgb(0, 0, 0);
197 | float: right;
198 | font-size: 28px;
199 | font-weight: bold;
200 | }
201 |
202 | .close:hover,
203 | .close:focus {
204 | color: rgb(38, 38, 38);
205 | text-decoration: none;
206 | cursor: pointer;
207 | }
208 |
209 | .hide {
210 | display: none;
211 | }
212 |
213 | .result-msg {
214 | margin-top: 10px;
215 | margin-bottom: 10px;
216 | font-size: larger;
217 | font-weight: bold;
218 | }
219 |
220 | .image_wrapper {
221 | position: relative;
222 | background: none;
223 | border: none;
224 | }
225 |
226 | .overlay {
227 | position: absolute;
228 | inset: 0;
229 | color: white;
230 | background: none;
231 | /* center overlay text */
232 | display: flex;
233 | align-items: center;
234 | justify-content: center;
235 | font-size: larger;
236 | }
237 |
238 | .blink {
239 | margin-top: 50px;
240 | animation: blink-animation 1s steps(5, start) infinite;
241 | -webkit-animation: blink-animation 1s steps(5, start) infinite;
242 | }
243 |
244 | @keyframes blink-animation {
245 | to {
246 | visibility: hidden;
247 | }
248 | }
249 |
250 | @-webkit-keyframes blink-animation {
251 | to {
252 | visibility: hidden;
253 | }
254 | }
255 |
256 | @media (max-width: 768px) {
257 | .overflow {
258 | overflow: auto;
259 | flex-wrap: wrap;
260 | }
261 | }
262 |
263 | .primary {
264 | min-width: 300px;
265 | }
266 |
267 | /* .burn-amt {
268 | padding: 4px;
269 | margin: 4px;
270 | width: 21%;
271 | border-radius: 4px;
272 | background: rgba(0, 0, 0, 0.131);
273 | border: none
274 | } */
275 |
276 | .burn-amt {
277 | text-align: center;
278 | font-family: var(--font-mono);
279 |
280 | }
281 | input[type=text] {
282 | width: 50%;
283 | padding: 12px 0px;
284 | margin: 8px 0;
285 | box-sizing: border-box;
286 | border: 1px solid rgba(204, 204, 204, 0.249);
287 | border-radius: 4px;
288 | background-color: #7f7f7f30;
289 | resize: none;
290 | font-family: var(--font-mono);
291 | text-align: center;
292 |
293 | }
294 | /* Chrome, Safari, Edge, Opera */
295 | input::-webkit-outer-spin-button,
296 | input::-webkit-inner-spin-button {
297 | -webkit-appearance: none;
298 | margin: 0;
299 | }
300 |
301 | /* Firefox */
302 | input[type=text] {
303 | -moz-appearance: textfield;
304 | }
305 |
306 | input[type=text]:focus {
307 | background-color: rgba(221, 180, 32, 0.237);
308 | outline: none;
309 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["./*"]
20 | }
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"]
24 | }
--------------------------------------------------------------------------------
/utils/api/crossmint.ts:
--------------------------------------------------------------------------------
1 | import { cleanDate, getRandomInt, wait } from "../utils";
2 | import fetch from 'node-fetch';
3 | import { IMG_100M, IMG_10M, IMG_1B, IMG_URIS } from "../constants";
4 |
5 | const CROSS_MINT_SECRET = process.env.CROSS_MINT_SECRET;
6 | const CROSS_MINT_PROJECT = process.env.CROSS_MINT_PROJECT;
7 |
8 | export const cmMintNft = async (
9 | pyro: string,
10 | amount: string,
11 | timestamp: string | number,
12 | tokenName: string,
13 | txid: string,
14 | burnQty: number
15 | ) => {
16 | if (!CROSS_MINT_SECRET || !CROSS_MINT_PROJECT) {
17 | console.error('Missing CrossMint credentials')
18 | return;
19 | }
20 |
21 |
22 |
23 | let imgIndex: number;
24 | let indexLabel: string;
25 | let imgUrl: string;
26 |
27 |
28 | if (burnQty < 10000000) {
29 | imgIndex = Math.floor(Math.random() * IMG_URIS.length);
30 | indexLabel = imgIndex.toString();
31 | imgUrl = IMG_URIS[imgIndex];
32 | }
33 | else if (burnQty < 100000000) {
34 | imgIndex = Math.floor(Math.random() * IMG_10M.length);
35 | indexLabel = (imgIndex + IMG_URIS.length).toString();
36 | imgUrl = IMG_10M[imgIndex];
37 | }
38 | else if (burnQty < 1000000000) {
39 | imgIndex = Math.floor(Math.random() * IMG_100M.length);
40 | indexLabel = (imgIndex + IMG_URIS.length + IMG_10M.length).toString();
41 | imgUrl = IMG_100M[imgIndex];
42 | }
43 | else {
44 |
45 | imgIndex = Math.floor(Math.random() * IMG_1B.length);
46 | indexLabel = (imgIndex + IMG_URIS.length + IMG_10M.length + IMG_100M.length).toString();
47 | imgUrl = IMG_1B[imgIndex];
48 | }
49 |
50 | const options = {
51 | method: 'POST',
52 | headers: {
53 | 'content-type': 'application/json',
54 | 'x-client-secret': CROSS_MINT_SECRET,
55 | 'x-project-id': CROSS_MINT_PROJECT
56 | },
57 | body: JSON.stringify({
58 | recipient: `solana:${pyro}`,
59 | metadata: {
60 | name: 'Proof of X - BONK Burn',
61 | symbol: 'BURN',
62 | seller_fee_basis_points: 500,
63 | image: imgUrl,
64 | description: 'Proof of Burn! This digital trophy commemorates your bold decision to burn a significant amount of BONK tokens, solidifying your status as a Solana Pyro. Keep this one-of-a-kind NFT in your digital collection as a constant reminder of your achievement.',
65 | attributes: [
66 | { trait_type: 'Proof of', value: 'Burn' },
67 | { trait_type: 'Burn Token', value: tokenName },
68 | { trait_type: 'Burn Amount', value: amount },
69 | { trait_type: 'wen', value: cleanDate(timestamp) },
70 | { trait_type: 'Pyro', value: pyro },
71 | { trait_type: 'Proof', value: txid },
72 | { trait_type: 'Variant', value: indexLabel }
73 | ]
74 | },
75 | properties: {
76 | files: [
77 | {
78 | "uri": imgUrl,
79 | "type": "image/png"
80 | }
81 | ],
82 | category: 'image',
83 | }
84 |
85 | })
86 | };
87 |
88 | try {
89 | let response = await fetch('https://staging.crossmint.com/api/2022-06-09/collections/default-solana/nfts', options);
90 | let result = (await response.json()) as CmMintResponse;
91 | let mintResults = await cmMintStatus(result.id);
92 | console.log(` - CMID: ${result.id}`)
93 | return {
94 | id: result.id,
95 | details: mintResults
96 | };
97 | }
98 | catch (err) {
99 | console.log(err);
100 | return;
101 | }
102 | }
103 | export const cmMintStatus = async (id: string, configOpts: MintStatusOptions = { maxRetries: 12, waitTime: 4800 }) => {
104 | if (!CROSS_MINT_SECRET || !CROSS_MINT_PROJECT) {
105 | console.error('Missing CrossMint credentials')
106 | return;
107 | }
108 | let success = false;
109 | let numTries = 0;
110 |
111 | const options = {
112 | method: 'GET',
113 | headers: {
114 | 'x-client-secret': CROSS_MINT_SECRET,
115 | 'x-project-id': CROSS_MINT_PROJECT
116 | }
117 | };
118 | try {
119 | while (!success && numTries < configOpts.maxRetries) {
120 | numTries++;
121 | await wait(configOpts.waitTime);
122 | let response = await fetch(`https://staging.crossmint.com/api/2022-06-09/collections/default-solana/nfts/${id}`, options)
123 | let result = (await response.json()) as CmMintResponse;
124 | if (result.onChain.status === "success") {
125 | success = true;
126 | console.log(`Returned success after ${numTries} attempt`);
127 | return result;
128 | }
129 | }
130 | return;
131 | }
132 | catch (err) {
133 | console.error(err);
134 | return;
135 | }
136 |
137 | }
138 |
139 | interface CmMintResponse {
140 | id: string,
141 | onChain: OnChain,
142 | metadata?: Metadata;
143 | }
144 |
145 | interface Metadata {
146 | name: string;
147 | symbol: string;
148 | description: string;
149 | seller_fee_basis_points: number;
150 | image: string;
151 | attributes?: unknown | null;
152 | properties: unknown;
153 | }
154 |
155 | interface OnChain {
156 | status: string;
157 | chain: string;
158 | mintHash?: string;
159 | owner?: string;
160 | }
161 |
162 | interface MintStatusOptions {
163 | maxRetries: number,
164 | waitTime: number
165 | }
--------------------------------------------------------------------------------
/utils/api/discord.ts:
--------------------------------------------------------------------------------
1 | import { Client, TextChannel } from 'discord.js';
2 | import { once } from "node:events"
3 |
4 | export async function sendDiscordMsg(message: string, channelId: string, token: string): Promise {
5 | if (!channelId || !token) {
6 | console.log('Unable to auth discord');
7 | return;
8 | }
9 |
10 | const client = new Client({ intents: ['GuildMessages', 'DirectMessages', 'MessageContent', 'Guilds'] });
11 |
12 | try {
13 | await client.login(token);
14 | await once(client, "ready");
15 | console.log(' ✅ DCRD: 🤖 Connection Established');
16 | }
17 | catch {
18 | console.log(` ❌ DCRD: Connection error.`);
19 | return;
20 | }
21 |
22 | try {
23 | await client.channels.fetch(channelId);
24 | const channel = client.channels.cache.get(channelId) as TextChannel;
25 | if (!channel) {
26 | console.log(` ❌ DCRD: Unable to find channel with id: ${channelId}`);
27 | return;
28 | }
29 | await channel.send(message);
30 | console.log(' ✅ DCRD: Message sent to server');
31 | return;
32 | }
33 | catch (error) {
34 | console.log(` ❌ DCRD: Client error: ${error}`);
35 | return;
36 | }
37 | finally {
38 | client.destroy();
39 | console.log(' ✅ DCRD: 🤖 Connection destroyed');
40 | return;
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/utils/constants.ts:
--------------------------------------------------------------------------------
1 | import { clusterApiUrl } from "@solana/web3.js";
2 |
3 | export const NETWORK = process.env.NEXT_PUBLIC_RPC
4 | ?? clusterApiUrl('mainnet-beta');
5 | export const TOKEN_MINT = process.env.NEXT_PUBLIC_TOKEN_MINT
6 | ?? 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263';
7 | export const MIN_BURN_AMT = Number(process.env.NEXT_PUBLIC_MIN_BURN_AMT)
8 | ?? 1000000;
9 | export const NUM_DECIMALS = Number(process.env.NEXT_PUBLIC_NUM_DECIMALS)
10 | ?? 5;
11 | export const TOKEN_NAME = process.env.NEXT_PUBLIC_TOKEN_NAME
12 | ?? 'BONK';
13 |
14 | export const IMG_URIS: string[] = [
15 | "https://arweave.net/brLBMvBhc1epsGMcGd3BIMHHNGp67O3w8BLFnuz6KB4?ext=png",
16 | "https://arweave.net/S40YOJwfdzGv6O9HfXYtYjs4eIyUYL4F3pmKLyOiFMA?ext=png",
17 | "https://arweave.net/spFzgmuTFbtHUFiUKAumcqm3NFCGuFYp_4pdZ84KK_Y?ext=png",
18 | "https://arweave.net/xCJDtMjSayg-Um1VDR8hSsUfAsACaNUjEE91BVaqRts?ext=png",
19 | "https://arweave.net/u6avFDK2qEZWv3Sy_jU2ylrBSQUYmNia3FEw6FEOd6M?ext=png",
20 | "https://arweave.net/d38Dr70BoX_vEO8TDj34SAYPBUEfLAbh-O-M4dB9Sfc?ext=png",
21 | "https://arweave.net/R1EyodNsavfvduSJ1_ye7G9eOR2_LxlP35Z8SLrg1IQ?ext=png",
22 | "https://arweave.net/CHBrx24sUS3p7q2pQX0b6wLEMfGtn2ByMEdQMoTsgE0?ext=png",
23 | "https://arweave.net/IQEleLuUqDLj-ZBZZ-0KxnzKZrVXS0E37YNFhoW6r4c?ext=png",
24 | "https://arweave.net/KP2yCkucDqTtbCK3605ci-tVIOi3xjKPEvzvRVw_Chs?ext=png",
25 | "https://arweave.net/l6mB4HlifviFqHf0SR1tjR0jPCl02rD49tq_0nndAOk?ext=png",
26 | "https://arweave.net/tTGfCn7LMX6YYZyCla7ARwMGX2LRx-y4f7Ax5py1ltM?ext=png",
27 | "https://arweave.net/Dl3FWkx6gAUq1v8TmEXbVCShMTSREVIt-7IVSrNYbp4?ext=png",
28 | "https://arweave.net/ILOszKonJC3HCnvwoJ7tMcynguk60vSOo7qoZlA7JCU?ext=png",
29 | "https://arweave.net/Zpd5CHxc1REwSPk5vbvaNA9t9DjyyxHpmi3imSQ4yQw?ext=png",
30 | "https://arweave.net/sggtJ61vSuKJWqUXYPJ_Tdb5Y_-vQrPIOZtPOEFjO-8?ext=png",
31 | "https://arweave.net/5xDgytUN0YVKOOwi-ayQ4DBEp8HX6wxKPnMmCX0Zfuk?ext=png"
32 | ];
33 |
34 | export const IMG_10M = [
35 | "https://arweave.net/yCwiBc6kWC0OWJREjwMS_bt6x0f_4AHpwRHfdDya4bY?ext=png",
36 | "https://arweave.net/HeBz713grRBUw4GCFkmVBUb3WPTUFVlpYgS-Ge83uvw?ext=png",
37 | "https://arweave.net/nzTco6k5BN4m_XILIfBbgFFgTtUp_dhj5Oj68mZgQd8?ext=png",
38 | "https://arweave.net/xIz4AoJuErFRW_fcMuo2gkew4MIC-R0vnW0J6HDBbBo?ext=png",
39 | "https://arweave.net/kjejWjciBwrhA1oQw32I3AIWDSoL0AtuIwQ9J_MZ2SY?ext=png",
40 | "https://arweave.net/bIbBOGp5p7mvLFYf1MgRmt-oJ3fkG-_63XiqGOOCcds?ext=png",
41 | "https://arweave.net/3ymWK_AyUxDv-gCIDJa_3ksNYyT1oSTE9dTHV6ql0u4?ext=png",
42 | "https://arweave.net/EVuxMxBfY4DOajkIPwINapDBVf9mKkxrOh0eGvizAEA?ext=png",
43 | "https://arweave.net/mB_1L3e5uVqLkNsdxAkSrRXZyXNvjHTOq0WphTwT7l0?ext=png",
44 | "https://arweave.net/xmURP6N-00afnp67sVazoH6cpWPjic9UCmiIFQEaGyE?ext=png",
45 | "https://arweave.net/VUTODfUvt_xGLnIHIDIqrqLcW5dRV1bNTCtXrmSP_1Q?ext=png",
46 | "https://arweave.net/2EneK8VvIbBAHtuM7zApIL4hNgvUOQt2e5aoN6XDbk4?ext=png"
47 | ];
48 |
49 | export const IMG_100M = [
50 | "https://arweave.net/ct7Is1qs-v9sqPY0ZYhytO13jGpUDc1X0GlDDfWjGQk?ext=png",
51 | "https://arweave.net/wFi8kuJuVq3n7DzUdCS1-c8p1qZhR4X0Zm8f1WYWfbA?ext=png",
52 | "https://arweave.net/HS3qeL-RZWgYsnU_Ouj8qdUfYPxgAg1xl5bPdOp966U?ext=png",
53 | "https://arweave.net/R74zd49t25RhDC25_Ld88RjQ-0LRxlc9iRt7TnEHZOI?ext=png",
54 | "https://arweave.net/xIg1v_3F3tiqlI2T_-OOtukEhSBMKKlBYCGR8ADCvMU?ext=png",
55 | "https://arweave.net/mIWaC-DsFcT6hokI5bnA3ibSYxQpXMGUiEVTS_HpxL4?ext=png",
56 | "https://arweave.net/GtjamlPtjcNhwBeZI4KCOV2y0bBb727z3gcgQKPh9vc?ext=png",
57 | "https://arweave.net/8CME_Lej1VsNFnc1o2fMRRrNrFBFiCNt8aIEc5e7Dog?ext=png",
58 | "https://arweave.net/sEgjzyiuea6Fck_IvrJZ4LVEbyrVdPxbGkj5kPAjs0s?ext=png",
59 | "https://arweave.net/ihaAF7qWj1RiSG_QKD2jQ9ntjrQwsQFst9fxSWIOryk?ext=png",
60 | "https://arweave.net/SacRIdi_sKbHyYgkMxUfCOS6vaeADVpFyQ6-yVAVHqU?ext=png",
61 | "https://arweave.net/lr7AOfYH1Sxbu6XoetezYtIjIFTwvENszI8vWE9cbU0?ext=png",
62 | "https://arweave.net/fd_F5GFXpQhysRlE1bouwa6aF4_6j4uglUVBgaYE3oU?ext=png"
63 | ];
64 |
65 | export const IMG_1B = ["https://arweave.net/ntkO4DPMthTKb3FbZMhKfUh7I--EGbjAM4oQ_iXtkd0?ext=png"];
--------------------------------------------------------------------------------
/utils/samples/my_cm.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Proof of X - BONK Burn",
3 | "symbol": "BURN",
4 | "description": "Proof of Burn! This digital trophy commemorates your bold decision to burn a significant amount of BONK tokens, solidifying your status as a Solana Pyro. Keep this one-of-a-kind NFT in your digital collection as a constant reminder of your achievement.",
5 | "seller_fee_basis_points": 500,
6 | "image": "https://arweave.net/R1EyodNsavfvduSJ1_ye7G9eOR2_LxlP35Z8SLrg1IQ?ext=png",
7 | "attributes": [
8 | {
9 | "trait_type": "Proof of",
10 | "value": "Burn"
11 | },
12 | {
13 | "trait_type": "Burn Token",
14 | "value": "BONK"
15 | },
16 | {
17 | "trait_type": "Burn Amount",
18 | "value": "10,055,679"
19 | },
20 | {
21 | "trait_type": "wen",
22 | "value": "2023-01-18"
23 | },
24 | {
25 | "trait_type": "Pyro",
26 | "value": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E"
27 | },
28 | {
29 | "trait_type": "Proof",
30 | "value": "JWSnRciiny8Z72GdYNcm2uWTa3dzovnuUmx3NqhoGcucSRJbh9yEX9osr5aNKwTwqeoGN6Ykodnqx4dvJ7AJoDR"
31 | },
32 | {
33 | "trait_type": "Variant",
34 | "value": 6
35 | }
36 | ],
37 | "properties": {
38 | "files": [],
39 | "category": "image",
40 | "creators": []
41 | }
42 | }
--------------------------------------------------------------------------------
/utils/samples/samplePOST.md:
--------------------------------------------------------------------------------
1 | curl -X POST "http://localhost:3000/api/burn" \
2 | -H "Content-Type: application/json" \
3 | -H "authorization: YOUR_AUTH" \
4 | -d '[
5 | {
6 | "accountData": [
7 | {
8 | "account": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E",
9 | "nativeBalanceChange": -5000,
10 | "tokenBalanceChanges": []
11 | },
12 | {
13 | "account": "CQ7T7r95gcD9w5zR5GKjGqQ7ugXfT1KLnwAM2g5XQXjN",
14 | "nativeBalanceChange": 0,
15 | "tokenBalanceChanges": [
16 | {
17 | "mint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
18 | "rawTokenAmount": {
19 | "decimals": 5,
20 | "tokenAmount": "-5567859787"
21 | },
22 | "tokenAccount": "CQ7T7r95gcD9w5zR5GKjGqQ7ugXfT1KLnwAM2g5XQXjN",
23 | "userAccount": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E"
24 | }
25 | ]
26 | },
27 | {
28 | "account": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
29 | "nativeBalanceChange": 0,
30 | "tokenBalanceChanges": []
31 | },
32 | {
33 | "account": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
34 | "nativeBalanceChange": 0,
35 | "tokenBalanceChanges": []
36 | }
37 | ],
38 | "description": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E burned 55678.59787 Bonk.",
39 | "events": {},
40 | "fee": 5000,
41 | "feePayer": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E",
42 | "instructions": [
43 | {
44 | "accounts": [
45 | "CQ7T7r95gcD9w5zR5GKjGqQ7ugXfT1KLnwAM2g5XQXjN",
46 | "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
47 | "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E"
48 | ],
49 | "data": "78EXLGcfEJTq",
50 | "innerInstructions": [],
51 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
52 | }
53 | ],
54 | "nativeTransfers": [],
55 | "signature": "JWSnRciiny8Z72GdYNcm2uWTa3dzovnuUmx3NqhoGcucSRJbh9yEX9osr5aNKwTwqeoGN6Ykodnqx4dvJ7AJoDR",
56 | "slot": 173237365,
57 | "source": "SOLANA_PROGRAM_LIBRARY",
58 | "timestamp": 1674099320,
59 | "tokenTransfers": [
60 | {
61 | "fromTokenAccount": "CQ7T7r95gcD9w5zR5GKjGqQ7ugXfT1KLnwAM2g5XQXjN",
62 | "fromUserAccount": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E",
63 | "mint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
64 | "toTokenAccount": "",
65 | "toUserAccount": "",
66 | "tokenAmount": 10055678.59787,
67 | "tokenStandard": "Fungible"
68 | }
69 | ],
70 | "transactionError": null,
71 | "type": "BURN"
72 | }
73 | ]'
--------------------------------------------------------------------------------
/utils/samples/sample_cm.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "4f2823c4-cb1a-488e-80d4-dc1c95b27ec7",
3 | "metadata": {
4 | "name": "My first Mint API NFT",
5 | "symbol": "",
6 | "description": "My NFT created via the mint API!",
7 | "seller_fee_basis_points": 0,
8 | "image": "https://www.crossmint.com/assets/crossmint/logo.png",
9 | "attributes": [],
10 | "properties": {
11 | "files": [],
12 | "category": "image",
13 | "creators": []
14 | }
15 | },
16 | "onChain": {
17 | "status": "success",
18 | "mintHash": "6jDgDn2h64xWusoU6jNhgoT5EQ73GJHsbPKt6Xf5XeXp",
19 | "owner": "5WpDyJ2fUnh7dHG2HRive2S8bS57WYodRX1wPKa1QRYZ",
20 | "chain": "solana"
21 | }
22 | }
--------------------------------------------------------------------------------
/utils/samples/sample_helius.json:
--------------------------------------------------------------------------------
1 | {
2 | "accountData": [
3 | {
4 | "account": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E",
5 | "nativeBalanceChange": -5000,
6 | "tokenBalanceChanges": []
7 | },
8 | {
9 | "account": "CQ7T7r95gcD9w5zR5GKjGqQ7ugXfT1KLnwAM2g5XQXjN",
10 | "nativeBalanceChange": 0,
11 | "tokenBalanceChanges": [
12 | {
13 | "mint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
14 | "rawTokenAmount": {
15 | "decimals": 5,
16 | "tokenAmount": "-5567859787"
17 | },
18 | "tokenAccount": "CQ7T7r95gcD9w5zR5GKjGqQ7ugXfT1KLnwAM2g5XQXjN",
19 | "userAccount": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E"
20 | }
21 | ]
22 | },
23 | {
24 | "account": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
25 | "nativeBalanceChange": 0,
26 | "tokenBalanceChanges": []
27 | },
28 | {
29 | "account": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
30 | "nativeBalanceChange": 0,
31 | "tokenBalanceChanges": []
32 | }
33 | ],
34 | "description": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E burned 55678.59787 Bonk.",
35 | "events": {},
36 | "fee": 5000,
37 | "feePayer": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E",
38 | "instructions": [
39 | {
40 | "accounts": [
41 | "CQ7T7r95gcD9w5zR5GKjGqQ7ugXfT1KLnwAM2g5XQXjN",
42 | "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
43 | "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E"
44 | ],
45 | "data": "78EXLGcfEJTq",
46 | "innerInstructions": [],
47 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
48 | }
49 | ],
50 | "nativeTransfers": [],
51 | "signature": "JWSnRciiny8Z72GdYNcm2uWTa3dzovnuUmx3NqhoGcucSRJbh9yEX9osr5aNKwTwqeoGN6Ykodnqx4dvJ7AJoDR",
52 | "slot": 173237365,
53 | "source": "SOLANA_PROGRAM_LIBRARY",
54 | "timestamp": 1674099320,
55 | "tokenTransfers": [
56 | {
57 | "fromTokenAccount": "CQ7T7r95gcD9w5zR5GKjGqQ7ugXfT1KLnwAM2g5XQXjN",
58 | "fromUserAccount": "Cw9PKetp1vodUfbL1whv2wDVTD5f5UhGq5GT6iFg4J1E",
59 | "mint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
60 | "toTokenAccount": "",
61 | "toUserAccount": "",
62 | "tokenAmount": 55678.59787,
63 | "tokenStandard": "Fungible"
64 | }
65 | ],
66 | "transactionError": null,
67 | "type": "BURN"
68 | }
--------------------------------------------------------------------------------
/utils/solana/fetchTokens.ts:
--------------------------------------------------------------------------------
1 | import { Connection, PublicKey } from "@solana/web3.js";
2 | export interface TokenAccounts {
3 | mintAddress: string,
4 | quantity: number
5 | }
6 |
7 | export async function getTokenBalance(wallet: PublicKey, solanaConnection: Connection, tokenMint: PublicKey):Promise {
8 | const tokenAccounts = await solanaConnection.getTokenAccountsByOwner(
9 | wallet,
10 | { mint: tokenMint },
11 | )
12 | const balances = await Promise.all(tokenAccounts.value.map(async ({ pubkey }) => {
13 | const balance = await solanaConnection.getTokenAccountBalance(pubkey)
14 | return balance.value.uiAmount ?? 0
15 | }))
16 |
17 | return balances.reduce((acc, curr) => acc + curr, 0)
18 | }
--------------------------------------------------------------------------------
/utils/solana/phantom.ts:
--------------------------------------------------------------------------------
1 | import { getAssociatedTokenAddress } from "@solana/spl-token";
2 | import { Connection, LAMPORTS_PER_SOL, PublicKey, Transaction, } from "@solana/web3.js";
3 | import { useEffect, useState } from "react";
4 | import { NETWORK, TOKEN_MINT } from '../constants';
5 | import { getTokenBalance } from "./fetchTokens";
6 |
7 | type DisplayEncoding = "utf8" | "hex";
8 | type PhantomEvent = "disconnect" | "connect";
9 | type PhantomRequestMethod =
10 | | "connect"
11 | | "disconnect"
12 | | "signTransaction"
13 | | "signAllTransactions"
14 | | "signMessage"
15 | | "signAndSendTransaction";
16 |
17 | interface ConnectOpts {
18 | onlyIfTrusted: boolean;
19 | }
20 |
21 | interface PhantomProvider {
22 | publicKey: PublicKey | null;
23 | isConnected: boolean | null;
24 | signTransaction: (transaction: Transaction) => Promise;
25 | signAndSendTransaction: (transaction: Transaction)=>Promise<{publicKey:string, signature: string}>;
26 | signAllTransactions: (transactions: Transaction[]) => Promise;
27 | signMessage: (
28 | message: Uint8Array | string,
29 | display?: DisplayEncoding
30 | ) => Promise;
31 | connect: (opts?: Partial) => Promise<{ publicKey: PublicKey }>;
32 | disconnect: () => Promise;
33 | on: (event: PhantomEvent, handler: (args: any) => void) => void;
34 | request: (method: PhantomRequestMethod, params: any) => Promise;
35 | }
36 | export interface UsePhantom {
37 | provider: PhantomProvider;
38 | balance: number | undefined;
39 | logs: string[];
40 | connect: () => Promise;
41 | disconnect: () => Promise;
42 | isConnected: PublicKey | null;
43 | }
44 |
45 | const getProvider = (): PhantomProvider | undefined => {
46 | if ("solana" in window) {
47 | const anyWindow: any = window;
48 | const provider = anyWindow.phantom.solana;
49 | if (provider.isPhantom) {
50 | return provider;
51 | }
52 | }
53 | // window.open("https://phantom.app/", "_blank");
54 | };
55 |
56 | function usePhantom() {
57 | const connection = new Connection(NETWORK);
58 | const [provider, setProvider] = useState();
59 | const [balance, setBalance] = useState();
60 | const [tokenBalance, setTokenBalance] = useState();
61 | const [ata, setAta] = useState();
62 | const [pubKey, setPubKey] = useState(null)
63 | const [logs, setLogs] = useState([]);
64 | const addLog = (log: string) => setLogs([...logs, log]);
65 | // eslint-disable-next-line consistent-return
66 | useEffect(() => {
67 | if (provider) {
68 | return () => {
69 | provider.disconnect();
70 | };
71 | }
72 | const theProvider = getProvider();
73 | setProvider(theProvider);
74 | }, [provider]);
75 |
76 | const connect = async () => {
77 | if (!provider) return;
78 | try {
79 | const res = await provider.connect();
80 | addLog(JSON.stringify(res));
81 | const publicKey = (res.publicKey);
82 | setPubKey(publicKey);
83 | connection.getBalance(publicKey).then((bal) => {
84 | const balance = bal / LAMPORTS_PER_SOL;
85 | console.log('connected. soL :', balance)
86 | setBalance(balance);
87 | });
88 | if (!TOKEN_MINT) return;
89 | getTokenBalance(publicKey, connection, new PublicKey(TOKEN_MINT)).then((balance)=>{
90 | console.log('token balance:', balance);
91 | setTokenBalance(balance);
92 | })
93 | let ata = await getAssociatedTokenAddress(new PublicKey(TOKEN_MINT), publicKey);
94 | setAta(ata);
95 | } catch (err) {
96 | addLog("Error: " + JSON.stringify(err));
97 | }
98 | };
99 |
100 | const disconnect = async () => {
101 | if (!provider) return;
102 | try {
103 | const res = await provider.disconnect();
104 | setPubKey(null);
105 | addLog(JSON.stringify(res));
106 | } catch (err) {
107 | console.warn(err);
108 | addLog("Error: " + JSON.stringify(err));
109 | }
110 | };
111 | return {
112 | connection,
113 | provider,
114 | balance,
115 | tokenBalance,
116 | ata,
117 | logs,
118 | pubKey,
119 | connect,
120 | disconnect,
121 | isConnected: provider && provider.publicKey,
122 | };
123 | }
124 |
125 | export default usePhantom;
--------------------------------------------------------------------------------
/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export function wait(ms: number) {
2 | return new Promise(resolve => setTimeout(resolve, ms));
3 | }
4 |
5 | export function shortHash(address?: string): string {
6 | if (!address) return '';
7 | return address.slice(0, 4) + '...' + address.slice(address.length - 4);
8 | }
9 |
10 | export function generateExplorerUrl(txId: string, cluster: string = 'devnet', address?: string) {
11 | if (!address) return `https://explorer.solana.com/tx/${txId}/?cluster=${cluster}`;
12 | return `https://explorer.solana.com/address/${address}?cluster=${cluster}`;
13 | }
14 |
15 | export function addLeadingZeros(n: number) {
16 | if (n <= 9) {
17 | return "0" + n;
18 | }
19 | return n
20 | }
21 |
22 | export function cleanDate(unix: number | string) {
23 | let date = new Date(Number(unix) * 1000);
24 | return date.getFullYear() + "-" + addLeadingZeros(date.getMonth() + 1) + "-" + addLeadingZeros(date.getDate());
25 | }
26 |
27 | export function getRandomInt(min:number, max:number) {
28 | min = Math.ceil(min);
29 | max = Math.floor(max);
30 | return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive
31 | }
--------------------------------------------------------------------------------