├── .gitattributes ├── src ├── files │ ├── Test.xlsx │ └── Test.csv ├── apis │ ├── airdrop.js │ └── file.js └── service │ └── index.js ├── .gitignore ├── vercel.json ├── README.md ├── index.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/files/Test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector41/solana-airdrop-backend/HEAD/src/files/Test.xlsx -------------------------------------------------------------------------------- /src/files/Test.csv: -------------------------------------------------------------------------------- 1 | address,amount 2 | 6YDwQX1cpRhobShS79R3jase3ZUNtKwCAJydQez4jiWT,1 3 | 9MA6grgJdbyWJakKHesZ6jUvDJJ4TGRtt7aDMqRFVd3v,2 4 | 4fNCxRYFv6VPTtjvujiEtnjVfYFasRTPmjSyoH1arr8G,1 5 | -------------------------------------------------------------------------------- /src/apis/airdrop.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | const airdrop = express.Router(); 3 | import service from "../service/index.js"; 4 | 5 | airdrop.get("/", async (req, res) => { 6 | 7 | let signature = await service.sendSPLTransaction(); 8 | return res.send({signature}); 9 | }) 10 | 11 | export default airdrop; -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | yarn.lock 26 | 27 | node_modules 28 | .env 29 | coverage 30 | coverage.json 31 | typechain 32 | 33 | #Truffle files 34 | /Truffle/build 35 | /Truffle/data -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "./index.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "headers": [ 10 | { 11 | "source": "/(.*)", 12 | "headers": [ 13 | { "key": "Access-Control-Allow-Credentials", "value": "true" }, 14 | { "key": "Access-Control-Allow-Origin", "value": "*" }, 15 | { "key": "Access-Control-Allow-Methods", "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" }, 16 | { "key": "Access-Control-Allow-Headers", "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" } 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # airdrop-backend-on-solana 2 | Simple SPL token airdrop using Node.js 3 | 4 | [THIS](https://github.com/blockchainexpertbc/airdrop-backend-on-solana) is the only official version of the bot 5 | 6 | I can only ensure the integrity of this version. If you use any fork, it will be at your own risks 7 | 8 | -blockchainexpertbc 9 | 10 | 11 | 12 | ## Installation 13 | ``` 14 | git clone https://github.com/vector41/solana-airdrop-backend.git 15 | cd ./solana-airdrop-backend 16 | npm install 17 | ``` 18 | ## Usage 19 | You can also run this script by using `npm start` 20 | 21 | ### .env 22 | - TOKEN_ADDRESS : The spl token address to transfer 23 | - SECRET_KEY : The scret key for gas fee to transfer spl token 24 | 25 | ### Example 26 | ``` 27 | npm start 28 | ``` 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import bodyParser from "body-parser"; 3 | import cors from "cors"; 4 | import { config } from "dotenv" 5 | import airdrop from "./src/apis/airdrop.js"; 6 | import file from "./src/apis/file.js"; 7 | 8 | config(); 9 | 10 | const app = express(); 11 | 12 | var corsOptions = { 13 | origin: "*" 14 | }; 15 | 16 | app.use(cors(corsOptions)); 17 | 18 | app.use(bodyParser.json()); 19 | 20 | app.use(bodyParser.urlencoded({ extended: true })); 21 | 22 | app.get("/", (req, res) => { 23 | return res.send("welcome!") 24 | }) 25 | 26 | app.get('/api', (req, res) => res.send('Api is ready')); 27 | 28 | app.use("/api/file", file); 29 | app.use("/api/airdrop", airdrop); 30 | 31 | const PORT = process.env.PORT || 4000; 32 | 33 | app.listen(PORT, () => { 34 | console.log(`Server is running on port ${PORT}.`); 35 | }); 36 | 37 | export default app; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airdrop-in-solana", 3 | "version": "1.0.0", 4 | "description": "Node.js + WEB3.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js" 8 | }, 9 | "keywords": [ 10 | "node.js", 11 | "express", 12 | "jwt", 13 | "authentication", 14 | "mongodb" 15 | ], 16 | "type": "module", 17 | "author": "bezkoder", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@project-serum/anchor": "^0.18.2", 21 | "@solana/spl-token": "^0.1.8", 22 | "@solana/web3.js": "^1.39.1", 23 | "bcryptjs": "^2.4.3", 24 | "body-parser": "^1.19.0", 25 | "bs58": "^5.0.0", 26 | "cors": "^2.8.5", 27 | "csv-parser": "^3.0.0", 28 | "dotenv": "^14.3.2", 29 | "express": "^4.17.1", 30 | "jsonwebtoken": "^8.5.1", 31 | "multer": "^1.4.4", 32 | "nodemon": "^2.0.15", 33 | "read-excel-file": "^5.2.28" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/apis/file.js: -------------------------------------------------------------------------------- 1 | import multer from "multer" 2 | import fs from "fs"; 3 | import express from "express"; 4 | const fileApi = express.Router(); 5 | 6 | const storage = multer.diskStorage({ 7 | 8 | destination: (req, file, cb) => { 9 | cb(null, './src/files'); 10 | }, 11 | filename: (req, file, cb) => { 12 | 13 | cb(null, file.originalname); 14 | } 15 | }); 16 | 17 | const upload = multer({storage: storage}); 18 | 19 | fileApi.post("/", upload.single('file'), (req, res) =>{ 20 | 21 | if (!req.file) { 22 | console.log("No file is available!"); 23 | return res.send({ 24 | success: false 25 | }); 26 | 27 | } else { 28 | 29 | setTimeout(() => { 30 | fs.rename(`./src/files/${req.file.originalname}`, './src/files/Test.xlsx', function(err) { 31 | if ( err ) { 32 | console.log(err) 33 | } 34 | }); 35 | }, 1500); 36 | 37 | return res.send({ 38 | "success": true 39 | }); 40 | } 41 | }) 42 | 43 | 44 | export default fileApi; 45 | -------------------------------------------------------------------------------- /src/service/index.js: -------------------------------------------------------------------------------- 1 | import { web3 } from "@project-serum/anchor"; 2 | import { TOKEN_PROGRAM_ID, Token } from "@solana/spl-token"; 3 | import csv from "csv-parser"; 4 | import fs from "fs"; 5 | import pkg from 'bs58'; 6 | const { decode } = pkg; 7 | 8 | class Service { 9 | 10 | constructor() { 11 | this.web3 = null; 12 | } 13 | 14 | toCluster(cluster) { 15 | switch (cluster) { 16 | case "devnet": 17 | case "testnet": 18 | case "mainnet-beta": { 19 | return cluster; 20 | } 21 | } 22 | throw new Error("Invalid cluster provided."); 23 | } 24 | 25 | async sendSPLTransaction() { 26 | 27 | let cluster = 'testnet'; 28 | let url = web3.clusterApiUrl(this.toCluster(cluster), true); 29 | let connection = new web3.Connection(url, 'processed'); 30 | 31 | // please replace secret key 32 | let SECRET_KEY = process.env.SECRET_KEY 33 | const Uin8bytes = decode(SECRET_KEY) 34 | const fromWallet = web3.Keypair.fromSecretKey(Uint8Array.from(Uin8bytes)); 35 | const tokenMintAddress = process.env.TOKEN_ADDRESS; 36 | 37 | fs.createReadStream('./src/files/Test.csv') 38 | .pipe(csv()) 39 | .on('data', async (row) => { 40 | 41 | const to = new web3.PublicKey(row?.address); 42 | await this.tokenTransfer(tokenMintAddress, fromWallet, to, connection, row?.amount) 43 | 44 | }) 45 | .on('end', () => { 46 | console.log('CSV file successfully processed'); 47 | }); 48 | } 49 | 50 | async tokenTransfer(tokenMintAddress, wallet, to, connection, amounts) { 51 | let decimals = web3.LAMPORTS_PER_SOL; 52 | 53 | const mintPublicKey = new web3.PublicKey(tokenMintAddress); 54 | const mintToken = new Token( 55 | connection, 56 | mintPublicKey, 57 | TOKEN_PROGRAM_ID, 58 | wallet 59 | ); 60 | const fromTokenAccount = await mintToken.getOrCreateAssociatedAccountInfo( 61 | wallet.publicKey 62 | ); 63 | let instructions = []; 64 | 65 | const dest = to; 66 | const destPublicKey = new web3.PublicKey(dest); 67 | // const associatedDestinationTokenAccount = await mintToken.getOrCreateAssociatedAccountInfo(destPublicKey) 68 | const associatedDestinationTokenAddr = await Token.getAssociatedTokenAddress( 69 | mintToken.associatedProgramId, 70 | mintToken.programId, 71 | mintPublicKey, 72 | destPublicKey 73 | ); 74 | const receiverAccount = await connection.getAccountInfo(associatedDestinationTokenAddr); 75 | if (receiverAccount === null) { 76 | instructions.push( 77 | Token.createAssociatedTokenAccountInstruction( 78 | mintToken.associatedProgramId, 79 | mintToken.programId, 80 | mintPublicKey, 81 | associatedDestinationTokenAddr, 82 | destPublicKey, 83 | wallet.publicKey 84 | ) 85 | ) 86 | } 87 | instructions.push( 88 | Token.createTransferInstruction( 89 | TOKEN_PROGRAM_ID, 90 | fromTokenAccount.address, 91 | associatedDestinationTokenAddr, 92 | wallet.publicKey, 93 | [], 94 | amounts * decimals 95 | ) 96 | ); 97 | const transaction = new web3.Transaction().add(...instructions); 98 | var signature = await web3.sendAndConfirmTransaction( 99 | connection, 100 | transaction, 101 | [wallet] 102 | ); 103 | console.log("SIGNATURE", signature); 104 | console.log("SUCCESS"); 105 | 106 | return signature; 107 | } 108 | } 109 | 110 | export default new Service(); --------------------------------------------------------------------------------