diff --git a/.gitignore b/.gitignore index 8f605e0..9b06a3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ package-lock.json yarn.lock -progress.json \ No newline at end of file +progress.json +.env \ No newline at end of file diff --git a/executeAirdrop.ts b/executeAirdrop.ts deleted file mode 100644 index 12d83ae..0000000 --- a/executeAirdrop.ts +++ /dev/null @@ -1,110 +0,0 @@ - -import * as web3 from '@solana/web3.js'; -import * as anchor from "@project-serum/anchor"; -import * as splToken from "@solana/spl-token"; -const fs = require('fs'); - -import { TokenInfo, readJson, writeJson } from "./prepareAirdrop"; - -const USE_MAINNET = false; -const MAGIC_EDEN_ADDRESS = "GUfCR9mK6azb9vcpsxgXyj7XRPAKJd4KMHTTVvtncGgp"; - -async function sendToken(connection: web3.Connection, dropInfo: TokenInfo, sender: web3.Keypair): Promise{ - - if(!dropInfo.sendableAmount || !dropInfo.sendableTokenMint || !dropInfo.owner) return ""; - - const mint = new web3.PublicKey(dropInfo.sendableTokenMint); - const owner = new web3.PublicKey(dropInfo.owner); - - - const transaction = new web3.Transaction(); - - //new splToken.Token(connection, mint, splToken.TOKEN_PROGRAM_ID, ) - - const destinationAccount = await splToken.Token.getAssociatedTokenAddress( - splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - splToken.TOKEN_PROGRAM_ID, mint, - owner, false); - - const sourceAccount = await splToken.Token.getAssociatedTokenAddress( - splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - splToken.TOKEN_PROGRAM_ID, mint, - sender.publicKey, false); - - - const destinationAccountInfo = await connection.getAccountInfo(destinationAccount); - const destTokenAccountMissing = !destinationAccountInfo; - if(destTokenAccountMissing){ - console.log("creating token account..."); - - const createATAinstruction = splToken.Token.createAssociatedTokenAccountInstruction( - splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - splToken.TOKEN_PROGRAM_ID, mint, - destinationAccount, owner, sender.publicKey); - - transaction.add(createATAinstruction); - } - - const transferInstruction = - splToken.Token.createTransferInstruction( - splToken.TOKEN_PROGRAM_ID, - sourceAccount, - destinationAccount, - sender.publicKey, - [], - dropInfo.sendableAmount - ) - transaction.add(transferInstruction); - - console.log("Sending form", sourceAccount.toBase58(),"to", destinationAccount.toBase58()); - - const txid = await web3.sendAndConfirmTransaction( - connection, transaction, [sender], { commitment: 'confirmed' }); - - return txid; -} - -export function loadWalletKey(keypairFile:string): web3.Keypair { - if (!keypairFile || keypairFile == '') { - throw new Error('Keypair is required!'); - } - const loaded = web3.Keypair.fromSecretKey( - new Uint8Array(JSON.parse(fs.readFileSync(keypairFile).toString())), - ); - console.log(`using wallet: ${loaded.publicKey}`); - return loaded; - } - -async function main() { - - const rpcHost = web3.clusterApiUrl(USE_MAINNET?"mainnet-beta":"devnet"); - const c = new anchor.web3.Connection(rpcHost); - - const allInfo = readJson(); - - const myKeypairFile = "C:\\Users\\loopc\\wkdir\\DEVkasD4qwUJQ8LS7rcJHcGDQQzrEtWgk2jB6v5FHngo.json"; - const sender = loadWalletKey(myKeypairFile); - - //const txid = await sendToken(c, allInfo[0], sender); - //console.log(txid); - //return; - - for (let i = 0; i setTimeout(f, 500)); - } - } - -} - -if (require.main === module) { - main(); -} \ No newline at end of file diff --git a/package.json b/package.json index b6bfabe..4d05244 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,15 @@ "dependencies": { "@project-serum/anchor": "^0.20.1", "@solana/spl-token": "^0.1.8", - "@solana/web3.js": "^1.32.0" + "@solana/web3.js": "^1.32.0", + "dotenv": "^14.3.2" + }, + "devDependencies": { + "ts-node": "^10.4.0", + "typescript": "^4.5.5" + }, + "scripts": { + "prepareAirdrop": "ts-node src/prepareAirdrop.ts", + "executeAirdrop": "ts-node src/executeAirdrop.ts" } } diff --git a/prepareAirdrop.ts b/prepareAirdrop.ts deleted file mode 100644 index f8f044c..0000000 --- a/prepareAirdrop.ts +++ /dev/null @@ -1,160 +0,0 @@ - -import * as web3 from '@solana/web3.js'; -import * as anchor from "@project-serum/anchor"; -import { assert } from 'console'; -import * as splToken from "@solana/spl-token"; -import { token } from '@project-serum/anchor/dist/cjs/utils'; -const fs = require('fs'); - -const VERIFIED_CREATOR = "AHx6cQKhJQ6vV9zhb37B7gKtRRGDuLtTfNWgvMiLDJp7"; -const TOKEN_TO_SEND = "toKPe7ENJiRBANPLnnp4dHs7ccFyHRg2oSEPdDfumg8"; -const AMOUNT_TO_SEND = 42000000000; // including decimans - -export const PROGRESS_FILE_PATH = "./progress.json"; - -export class TokenInfo { - metadataAccount: string; - nftTokenMint: string; - nftName: string | undefined; - owner: string | undefined; - sendableTokenMint: string | undefined; - sendableAmount: number | undefined; - txid: string | undefined; - - constructor(metadataAccount: string, tokenMint: string) { - this.metadataAccount = metadataAccount; - this.nftTokenMint = tokenMint; - } - - show(){ - console.log(this.metadataAccount + " -> " + this.nftTokenMint +' -> '+ this.owner); - } -} - -export function writeJson(data: TokenInfo[]){ - let json = JSON.stringify(data); - fs.writeFileSync(PROGRESS_FILE_PATH, json); -} - -export function readJson(): TokenInfo[] { - return JSON.parse(fs.readFileSync(PROGRESS_FILE_PATH).toString()); -} - -async function getOwnerForNFT(c: web3.Connection, tokenMintString: string) : Promise{ - - const tokenMint = new anchor.web3.PublicKey(tokenMintString); - //const accountInfo = await c.getAccountInfo(tokenMint); - - const largestAccouts = await c.getTokenLargestAccounts(tokenMint); - const onlyHolder : web3.TokenAccountBalancePair[] = largestAccouts!.value.filter((tokenHolder: web3.TokenAccountBalancePair) => tokenHolder.uiAmount); - assert(onlyHolder.length == 1); - const NFTTokenAccount = onlyHolder[0].address; - //console.log(NFTTokenAccount.toBase58()); - - const tokenAccountInfo = await c.getAccountInfo(NFTTokenAccount); - const owner = new web3.PublicKey(tokenAccountInfo!.data.slice(32, 64)); - - //console.log(owner.toBase58()); - return owner; -} - -async function getAllNFTsForCreator(c: web3.Connection, verifiedCreator: string) : Promise{ - - const config : web3.GetProgramAccountsConfig = { - commitment: undefined, - encoding: "base64", - dataSlice: undefined, - filters: [ - { - "memcmp": { - "offset": 1 + // key - 32 + // update auth - 32 + // mint - 4 + // name string length - 32 + //MAX_NAME_LENGTH + // name - 4 + // uri string length - 200 + // MAX_URI_LENGTH + // uri* - 4 + // symbol string length - 10 + // MAX_SYMBOL_LENGTH + // symbol - 2 + // seller fee basis points - 1 + // whether or not there is a creators vec - 4, // creators - "bytes": verifiedCreator - } - } - ] - } - - const TOKEN_METADATA_PROGRAM_ID = new web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); - const accountList = await c.getProgramAccounts(TOKEN_METADATA_PROGRAM_ID, config); - - //console.log(accountList); - - const allInfo : TokenInfo[] = []; - - for (let i =0; i { - element.sendableAmount = AMOUNT_TO_SEND; - element.sendableTokenMint = TOKEN_TO_SEND; - }); -} - -async function main(){ - - - const rpcHost = "https://ssc-dao.genesysgo.net/" - const c = new anchor.web3.Connection(rpcHost); - - //const owner = await getOwnerForNFT(c, "AP7VntKBj4253RV6ktMrZJst5JFFmrQnHy29HC7rHvd"); - //console.log(owner.toBase58()); - - if(!fs.existsSync(PROGRESS_FILE_PATH)){ - const allInfo = await getAllNFTsForCreator(c, VERIFIED_CREATOR); - writeJson(allInfo); - console.log("file saved"); - } - - const allInfo = readJson(); - - console.log("finding owners...") - allInfo.forEach(async tokenInfo => { - if (!tokenInfo.owner) { - tokenInfo.owner = await (await getOwnerForNFT(c, tokenInfo.nftTokenMint)).toBase58(); - writeJson(allInfo); - } - }); - - prepareSend(allInfo); - writeJson(allInfo); - - console.log("DONE"); -} - -if (require.main === module) { - main(); -} \ No newline at end of file diff --git a/src/executeAirdrop.ts b/src/executeAirdrop.ts new file mode 100644 index 0000000..37a27eb --- /dev/null +++ b/src/executeAirdrop.ts @@ -0,0 +1,132 @@ +import { + Token, + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; + +import { + Connection, + Cluster, + Keypair, + PublicKey, + Transaction, + sendAndConfirmTransaction, + clusterApiUrl, +} from "@solana/web3.js"; + +import fs from "fs"; + +import { TokenInfo } from "./prepareAirdrop"; +import { readJson, writeJson } from "./utils/file"; +import { config } from "dotenv"; + +const MAGIC_EDEN_ADDRESS = "GUfCR9mK6azb9vcpsxgXyj7XRPAKJd4KMHTTVvtncGgp"; + +async function sendToken( + connection: Connection, + { sendableAmount, sendableTokenMint, owner }: TokenInfo, + sender: Keypair +): Promise { + if (!sendableAmount || !sendableTokenMint || !owner) return undefined; + + const mint = new PublicKey(sendableTokenMint); + const receiver = new PublicKey(owner); + + const transaction = new Transaction(); + + const destinationAccount = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + mint, + receiver + ); + + const sourceAccount = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + mint, + sender.publicKey + ); + + const destinationAccountInfo = await connection.getAccountInfo( + destinationAccount + ); + + if (!destinationAccountInfo) { + console.log("creating token account..."); + + const createATAinstruction = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + mint, + destinationAccount, + receiver, + sender.publicKey + ); + + transaction.add(createATAinstruction); + } + + const transferInstruction = Token.createTransferInstruction( + TOKEN_PROGRAM_ID, + sourceAccount, + destinationAccount, + sender.publicKey, + [], + sendableAmount + ); + + transaction.add(transferInstruction); + + console.log( + "Sending from", + sourceAccount.toBase58(), + "to", + destinationAccount.toBase58() + ); + + const txid = await sendAndConfirmTransaction(connection, transaction, [ + sender, + ]); + + return txid; +} + +export function loadWalletKey(keypairFile: string): Keypair { + if (!keypairFile || keypairFile == "") { + throw new Error("Keypair is required!"); + } + const loaded = Keypair.fromSecretKey( + new Uint8Array(JSON.parse(fs.readFileSync(keypairFile).toString())) + ); + console.log(`Using wallet: ${loaded.publicKey}`); + return loaded; +} + +(async function main() { + config(); + + const rpcHost = clusterApiUrl(process.env.RPC_ENV! as Cluster); + const connection = new Connection(rpcHost); + + const allInfo = readJson(); + + const myKeypairFile = process.env.KEYPAIR_PATH; + + if (!myKeypairFile) throw new Error("Keypair not present"); + + const sender = loadWalletKey(myKeypairFile); + + const nonMagicEdenInfo = allInfo.filter( + (info) => info.owner !== MAGIC_EDEN_ADDRESS + ); + + for (const info of nonMagicEdenInfo) { + if (!info.txid) { + const txid = await sendToken(connection, info, sender); + info.txid = txid; + writeJson(allInfo); + console.log(`Tokens sent to ${info.owner.toString()}. Tx Hash: ${txid}`); + } + } +})(); diff --git a/src/prepareAirdrop.ts b/src/prepareAirdrop.ts new file mode 100644 index 0000000..a9c67fe --- /dev/null +++ b/src/prepareAirdrop.ts @@ -0,0 +1,66 @@ +import fs from "fs"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { writeJson, readJson } from "./utils/file"; +import { getAllNFTsForCreator, getOwnerForNFT } from "./utils/nft"; +import { + AMOUNT_TO_SEND, + TOKEN_TO_SEND, + PROGRESS_FILE_PATH, + VERIFIED_CREATOR, +} from "./utils/constants"; + +export class TokenInfo { + metadataAccount: string; + nftTokenMint: string; + nftName: string | undefined; + owner: string | undefined; + sendableTokenMint: string | undefined; + sendableAmount: number | undefined; + txid: string | undefined; + + constructor(metadataAccount: string, tokenMint: string) { + this.metadataAccount = metadataAccount; + this.nftTokenMint = tokenMint; + } + + show() { + console.log( + this.metadataAccount + " -> " + this.nftTokenMint + " -> " + this.owner + ); + } +} + +async function prepareSend(data: TokenInfo[]) { + data.forEach((element) => { + element.sendableAmount = AMOUNT_TO_SEND; + element.sendableTokenMint = TOKEN_TO_SEND.toString(); + }); +} + +(async function main() { + const rpcHost = "https://ssc-dao.genesysgo.net/"; + const c = new Connection(rpcHost); + + if (!fs.existsSync(PROGRESS_FILE_PATH)) { + const allInfo = await getAllNFTsForCreator(c, VERIFIED_CREATOR); + writeJson(allInfo); + console.log("file saved"); + } + + const allInfo = readJson(); + + console.log("finding owners..."); + allInfo.forEach(async (tokenInfo) => { + if (!tokenInfo.owner) { + tokenInfo.owner = ( + await getOwnerForNFT(c, new PublicKey(tokenInfo.nftTokenMint)) + ).toBase58(); + writeJson(allInfo); + } + }); + + prepareSend(allInfo); + writeJson(allInfo); + + console.log("DONE"); +})(); diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..7e1fd4e --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,15 @@ +import { PublicKey } from "@solana/web3.js"; + +export const VERIFIED_CREATOR = new PublicKey( + "AHx6cQKhJQ6vV9zhb37B7gKtRRGDuLtTfNWgvMiLDJp7" +); + +export const TOKEN_TO_SEND = new PublicKey( + "5gCuvpcxHUdZ3sEFDodjEAcxKBvp7TDMkfLT9veKsg4e" +); + +export const TOKEN_DECIMALS = 9; + +export const AMOUNT_TO_SEND = 42 * 10 ** TOKEN_DECIMALS; // including decimals + +export const PROGRESS_FILE_PATH = `${__dirname}/../../progress.json`; diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000..5139b02 --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,12 @@ +import fs from "fs"; +import { TokenInfo } from "../prepareAirdrop"; +import { PROGRESS_FILE_PATH } from "./constants"; + +export function writeJson(data: TokenInfo[]) { + let json = JSON.stringify(data, null, 2); + fs.writeFileSync(PROGRESS_FILE_PATH, json); +} + +export function readJson(): TokenInfo[] { + return JSON.parse(fs.readFileSync(PROGRESS_FILE_PATH).toString()); +} diff --git a/src/utils/nft.ts b/src/utils/nft.ts new file mode 100644 index 0000000..63f7fd6 --- /dev/null +++ b/src/utils/nft.ts @@ -0,0 +1,91 @@ +import { web3 } from "@project-serum/anchor"; +import { assert } from "console"; +import { + Connection, + GetProgramAccountsConfig, + PublicKey, +} from "@solana/web3.js"; +import { TokenInfo } from "../prepareAirdrop"; + +export async function getAllNFTsForCreator( + c: Connection, + verifiedCreator: PublicKey +): Promise { + const config: GetProgramAccountsConfig = { + commitment: undefined, + encoding: "base64", + dataSlice: undefined, + filters: [ + { + memcmp: { + offset: + 1 + // key + 32 + // update auth + 32 + // mint + 4 + // name string length + 32 + //MAX_NAME_LENGTH + // name + 4 + // uri string length + 200 + // MAX_URI_LENGTH + // uri* + 4 + // symbol string length + 10 + // MAX_SYMBOL_LENGTH + // symbol + 2 + // seller fee basis points + 1 + // whether or not there is a creators vec + 4, // creators + bytes: verifiedCreator.toString(), + }, + }, + ], + }; + + const TOKEN_METADATA_PROGRAM_ID = new web3.PublicKey( + "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + ); + const accountList = await c.getProgramAccounts( + TOKEN_METADATA_PROGRAM_ID, + config + ); + + const allInfo: TokenInfo[] = []; + + for (let i = 0; i < accountList.length; i++) { + const metadataAccountPK = accountList[i].pubkey.toBase58(); + + const tokenMint = new web3.PublicKey( + accountList[i].account.data.slice(1 + 32, 1 + 32 + 32) + ).toBase58(); + + allInfo[i] = new TokenInfo(metadataAccountPK, tokenMint); + + const nameLenght = accountList[i].account.data.readUInt32LE(1 + 32 + 32); + const nameBuffer = accountList[i].account.data.slice( + 1 + 32 + 32 + 4, + 1 + 32 + 32 + 4 + 32 + ); + + let name = ""; + for (let j = 0; j < nameLenght; j++) { + if (nameBuffer.readUInt8(j) == 0) break; + name += String.fromCharCode(nameBuffer.readUInt8(j)); + } + allInfo[i].nftName = name; + } + return allInfo; +} + +export async function getOwnerForNFT( + c: web3.Connection, + tokenMint: PublicKey +): Promise { + const largestAccouts = await c.getTokenLargestAccounts(tokenMint); + const onlyHolder: web3.TokenAccountBalancePair[] = + largestAccouts!.value.filter( + (tokenHolder: web3.TokenAccountBalancePair) => tokenHolder.uiAmount + ); + + assert(onlyHolder.length == 1); + const NFTTokenAccount = onlyHolder[0].address; + + const tokenAccountInfo = await c.getAccountInfo(NFTTokenAccount); + const owner = new web3.PublicKey(tokenAccountInfo!.data.slice(32, 64)); + return owner; +}