From 8c14e97c83b37bcff1e3ac652b45b10007ef393c Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 27 May 2022 21:47:35 +0700 Subject: [PATCH] Scripts for whitelist airdrop --- Bot/Bot.fsproj | 2 + Bot/Scripts/Airdrop.fsx | 180 ++++++++++++++++++++++++++++++++++++++ Bot/Scripts/Whitelist.fsx | 73 ++++++++++++++++ paket.dependencies | 3 + paket.lock | 16 +++- 5 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 Bot/Scripts/Airdrop.fsx create mode 100644 Bot/Scripts/Whitelist.fsx diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index 95eeab5..0a13f65 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -28,6 +28,8 @@ + + diff --git a/Bot/Scripts/Airdrop.fsx b/Bot/Scripts/Airdrop.fsx new file mode 100644 index 0000000..0202bd3 --- /dev/null +++ b/Bot/Scripts/Airdrop.fsx @@ -0,0 +1,180 @@ +#load "/home/joe/Development/DegenzGame/.paket/load/net6.0/main.group.fsx";; + +open System +open System.IO +open Npgsql.FSharp +open dotenv.net +open Solnet.Rpc +open Solnet.Rpc.Models +open Solnet.Rpc.Builders +open Solnet.Wallet +open Solnet.Programs +open Solnet.KeyStore + +let devEnv = DotEnv.Read(DotEnvOptions(envFilePaths = [ "./.dev.env" ])) + +let ( _ , devConnStr )= devEnv.TryGetValue("DATABASE_URL") + +let keystore = SolanaKeyStoreService() +let file = File.ReadAllText("/home/joe/.config/solana/devnet.json") +let authority = keystore.RestoreKeystore(file) + +//let rpcClient = ClientFactory.GetClient("https://still-empty-field.solana-mainnet.quiknode.pro/5b1b6b5c913ec79a20bef19d5ba5f63023e470d6/") +let rpcClient = ClientFactory.GetClient(Cluster.DevNet) + +let mintAccount = PublicKey("2iS6gcoB5VhiLC4eNB7NdcaLgEHjLrXHYpz7T2JMGBDw") +//let associatedTokenAccountOwner = PublicKey("GutKESfJw8PDMbFVqByxTr4f5TUSHUVmkf5gtsWWWqrU") + +type AirdropStatus = + | Hold = 0 + | Ready = 1 + | Pending = 2 + | Error = 3 + | PendingError = 4 + | Completed = 5 + | Verified = 6 + +let updateError wallet msg = + devConnStr + |> Sql.connect + |> Sql.parameters [ "wallet" , Sql.string wallet ; "msg" , Sql.string msg ] + |> Sql.query """ + UPDATE crypto SET error_msg = @msg, status = 'Error' WHERE crypto.wallet_address = @wallet; + """ + |> Sql.executeNonQueryAsync + +let updatePendingError wallet msg txId = + devConnStr + |> Sql.connect + |> Sql.parameters [ "wallet" , Sql.string wallet ; "msg" , Sql.string msg ; "txId" , Sql.string txId ] + |> Sql.query """ + UPDATE crypto SET error_msg = @msg, pending_tx = @txId, status = 'PendingError' WHERE crypto.wallet_address = @wallet; + """ + |> Sql.executeNonQueryAsync + +let updatePending wallet txId = + devConnStr + |> Sql.connect + |> Sql.parameters [ "wallet" , Sql.string wallet ; "txId" , Sql.string txId ] + |> Sql.query """ + UPDATE crypto SET pending_tx = @txId, status = 'Pending' WHERE crypto.wallet_address = @wallet; + """ + |> Sql.executeNonQueryAsync + +let updateCompleted wallet txId = + devConnStr + |> Sql.connect + |> Sql.parameters [ "wallet" , Sql.string wallet ; "txId" , Sql.string txId ] + |> Sql.query """ + UPDATE crypto SET successful_tx = pending_tx, status = 'Completed' WHERE crypto.wallet_address = @wallet; + """ + |> Sql.executeNonQueryAsync + +let updateVerified wallet txId amount = + devConnStr + |> Sql.connect + |> Sql.parameters [ "wallet" , Sql.string wallet ; "txId" , Sql.string txId ; "amount" , Sql.int amount ] + |> Sql.query """ + UPDATE crypto SET pending_tx = @txId, status = 'Verified', total_dropped = @amount WHERE crypto.wallet_address = @wallet; + """ + |> Sql.executeNonQueryAsync + + +let getTokenCountToDrop wallet = + task { + let! wl = + devConnStr + |> Sql.connect + |> Sql.parameters [ "wallet" , Sql.string wallet ] + |> Sql.query """ + SELECT has_wl, has_og FROM crypto WHERE wallet_address = @wallet; + """ + |> Sql.executeRowAsync (fun reader -> {| HasWhitelist = reader.bool "has_wl" ; HasOg = reader.bool "has_og" |}) + return (if wl.HasWhitelist then 1uL else 0uL) + (if wl.HasOg then 2uL else 0uL) + } + +// TODO: Change was_successful to status and check if attempted, pending, errored, or completed +let executeDrop (wallet : string) = + let associatedTokenAccountOwner = PublicKey(wallet) + let associatedTokenAccount = AssociatedTokenAccountProgram.DeriveAssociatedTokenAccount(associatedTokenAccountOwner, mintAccount) + let log msg = printfn $"{wallet} || {msg}" + + log $"ATA: {associatedTokenAccount.Key}" + let buildTransaction (block : LatestBlockHash) amount (targetWallet : string) = + TransactionBuilder() + .SetRecentBlockHash(block.Blockhash) + .SetFeePayer(authority.Account) + .AddInstruction(AssociatedTokenAccountProgram.CreateAssociatedTokenAccount( + authority.Account, + PublicKey(targetWallet), + mintAccount)) + .AddInstruction(TokenProgram.Transfer( + PublicKey("CRa7GCMUaB4np32XoTD2sEyCXFVfYKKF4JRPkQTwV3EY"), + associatedTokenAccount, + amount, + authority.Account)) + .Build(authority.Account); + task { +// let streamingClient = ClientFactory.GetStreamingClient("wss://still-empty-field.solana-mainnet.quiknode.pro/5b1b6b5c913ec79a20bef19d5ba5f63023e470d6/") + let streamingClient = ClientFactory.GetStreamingClient(Cluster.DevNet) + do! streamingClient.ConnectAsync() + let blockHash = rpcClient.GetLatestBlockHash() + let! amount = getTokenCountToDrop wallet + log $"Dropping {amount} tokens" + let tx = buildTransaction blockHash.Result.Value amount wallet + let! tx = rpcClient.SendTransactionAsync(tx) + if tx.ErrorData <> null then + let! _ = updateError wallet (tx.ErrorData.Logs |> String.concat "\n") + () + log $"Transaction error: {wallet}" + return () + elif String.IsNullOrWhiteSpace(tx.Result) then + let msg = $"Transaction did not have an ID but the ErrorData was null: {tx.Reason}" + let! _ = updateError wallet msg + log $"Odd Transaction Error" + return () + else + log $"Successful, now waiting for RPC" + let! _ = updatePending wallet tx.Result + let! _ = streamingClient.SubscribeSignatureAsync(tx.Result, fun sub result -> + if result.Value.Error = null then + log "RPC Finished" + task { + let! _ = updateCompleted wallet tx.Result + log "Getting Transaction and Token Balance" + let! txInfo = rpcClient.GetTransactionAsync(tx.Result) + let! tokenBalance = rpcClient.GetTokenAccountBalanceAsync(associatedTokenAccount) + + log $"Transaction Successful? {txInfo.WasSuccessful} - Token Balance {tokenBalance.Result.Value.AmountUlong}" + if txInfo.WasSuccessful = true && tokenBalance.Result.Value.AmountUlong = amount then + let! _ = updateVerified wallet tx.Result (int amount) + () + return () + } |> Async.AwaitTask |> Async.Start + else + let msg = $"Got an error, let's check it out {result.Value.Error}" + updatePendingError wallet msg tx.Result |> Async.AwaitTask |> Async.Ignore |> Async.Start) + return () + } + +let targetWallets = + devConnStr + |> Sql.connect + |> Sql.query """ + SELECT wallet_address FROM crypto WHERE status = 'Ready'; + """ + |> Sql.execute (fun reader -> reader.string "wallet_address") + +printfn $"Got target wallets: {targetWallets.Length}" + +// "GK7rkZYrdAEpTm9n9TkHWK1T5nDXeRfVUVfcHQwSDyuJ" +let asyncs = + targetWallets + |> List.map executeDrop + |> List.map Async.AwaitTask + +Async.Parallel ( asyncs , 10 ) |> Async.StartChild + + +Console.ReadLine() |> ignore + diff --git a/Bot/Scripts/Whitelist.fsx b/Bot/Scripts/Whitelist.fsx new file mode 100644 index 0000000..8d7fe72 --- /dev/null +++ b/Bot/Scripts/Whitelist.fsx @@ -0,0 +1,73 @@ +#load "/home/joe/Development/DegenzGame/.paket/load/net6.0/main.group.fsx";; + +open Npgsql.FSharp +open DSharpPlus +open dotenv.net + +let prodEnv = DotEnv.Read(DotEnvOptions(envFilePaths = [ "./.prod.env" ])) +let devEnv = DotEnv.Read(DotEnvOptions(envFilePaths = [ "./.dev.env" ])) + +let ( _ , prodConnStr )= prodEnv.TryGetValue("DATABASE_URL") +let ( _ , devConnStr )= devEnv.TryGetValue("DATABASE_URL") +let ( _ , adminBotToken )= prodEnv.TryGetValue("TOKEN_ADMINBOT") + +let botConfig = DiscordConfiguration() +botConfig.TokenType <- TokenType.Bot +botConfig.Intents <- DiscordIntents.All +botConfig.Token <- adminBotToken + +printfn "Connecting" +let bot = new DiscordClient(botConfig) +bot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously + +printfn "Getting Guild" +let prodGuild = 933888229776703559uL +let guild = bot.GetGuildAsync(prodGuild) |> Async.AwaitTask |> Async.RunSynchronously +printfn "Getting Members" +let users = guild.GetAllMembersAsync() |> Async.AwaitTask |> Async.RunSynchronously +printfn $"Total Members: {users.Count}" + +let whitelisted = + users + |> Seq.filter (fun u -> u.Roles |> Seq.exists (fun role -> role.Name.Contains("Confirmed"))) + |> Seq.toList + +printfn $"Total Whitelist Confirmed: {whitelisted.Length}" + +printfn "Getting Wallet Addresses:" +let walletAddresses = + prodConnStr + |> Sql.connect + |> Sql.query """ + SELECT DISTINCT discord_id, display_name, wallet_address FROM "user" WHERE wallet_address IS NOT NULL; + """ + |> Sql.execute (fun reader -> {| Id = reader.string "discord_id" |> uint64 ; Name = reader.string "display_name" ; Wallet = reader.string "wallet_address" |}) + +printfn $"Total Wallet Addresses: {walletAddresses.Length}" + +let insert did name wallet wl og = + devConnStr + |> Sql.connect + |> Sql.parameters [ "did" , Sql.string (string did) ; "name" , Sql.string name ; "wallet" , Sql.string wallet + "wl" , Sql.bool wl ; "og" , Sql.bool og ] + |> Sql.query """ + INSERT INTO crypto(discord_id, display_name, wallet_address, has_wl, has_og) VALUES (@did, @name, @wallet, @wl, @og) + ON CONFLICT (wallet_address) DO + UPDATE SET display_name = @name, discord_id = @did , has_wl = @wl , has_og = @og WHERE crypto.wallet_address = @wallet; + """ + |> Sql.executeNonQuery + +printfn "Inserting" +async { + for user in whitelisted do + for wa in walletAddresses do + if wa.Id = user.Id then + let hasWL = user.Roles |> Seq.exists (fun role -> role.Name = "Whitelist Confirmed") + let hasOG = user.Roles |> Seq.exists (fun role -> role.Name = "OG Whitelist Confirmed") + try + let _ = insert wa.Id wa.Name wa.Wallet hasWL hasOG + () + with ex -> printfn $"{ex.Message}" +} |> Async.RunSynchronously + +bot.DisconnectAsync() |> Async.AwaitTask |> Async.RunSynchronously \ No newline at end of file diff --git a/paket.dependencies b/paket.dependencies index 5a03d03..c393de4 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -13,4 +13,7 @@ nuget MongoDB.Driver nuget dotenv.net 3.1.1 nuget Npgsql.FSharp nuget mixpanel-csharp 5.0.0 +nuget Solnet.Extensions +nuget Solnet.KeyStore +nuget Solnet.Programs nuget Solnet.Rpc diff --git a/paket.lock b/paket.lock index cb68a6b..5687dea 100644 --- a/paket.lock +++ b/paket.lock @@ -176,11 +176,21 @@ NUGET SharpCompress (0.30.1) System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) System.Text.Encoding.CodePages (>= 5.0) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) (== netstandard2.1) - Solnet.Rpc (6.0.10) + Solnet.Extensions (6.0.11) + Solnet.Programs (>= 6.0.11) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Solnet.Rpc (>= 6.0.11) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Solnet.Wallet (>= 6.0.11) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Solnet.KeyStore (6.0.11) + Chaos.NaCl.Standard (>= 1.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Portable.BouncyCastle (>= 1.9) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Solnet.Wallet (>= 6.0.11) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Solnet.Programs (6.0.11) + Solnet.Rpc (>= 6.0.11) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Solnet.Rpc (6.0.11) Microsoft.Extensions.Logging (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Microsoft.Extensions.Logging.Console (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Solnet.Wallet (>= 6.0.10) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Solnet.Wallet (6.0.10) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Solnet.Wallet (>= 6.0.11) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Solnet.Wallet (6.0.11) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Chaos.NaCl.Standard (>= 1.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Portable.BouncyCastle (>= 1.9) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) System.Buffers (4.5.1)