├── .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 | 168 | {!isConnected ? : 183 |
187 |

188 | {((tokenBalance ?? 0) >= MIN_BURN_AMT) ? '🔥Click Dog to Burn🔥' : 'MORE BONK NEEDED'} 189 |

190 |

191 | Burn 1M+ BONK ->Get NFT
WARNING: Burn is irreversible 192 |


193 |

194 | 🟢 Connected to {shortHash(pubKey?.toString())}
195 |

196 | 197 |
198 | 199 |
Burn Amt:
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 | } --------------------------------------------------------------------------------