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)