From a578978c959b4bdab811b2d236c8ac879e9f400e Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Tue, 24 Jun 2025 15:45:49 +0700 Subject: [PATCH] Revert "Cleaning up" This reverts commit 0db69bf72b0b090edc22d0e682f8f3bb4356dc3e. --- Airdrop.fsx | 179 +++++++++++++++++++++++++++++++ Bot/Scripts/Airdrop.fsx | 188 +++++++++++++++++++++++++++++++++ Bot/Scripts/GetWhitelisted.fsx | 148 ++++++++++++++++++++++++++ Bot/Scripts/TransferNFT.fsx | 96 +++++++++++++++++ 4 files changed, 611 insertions(+) create mode 100644 Airdrop.fsx create mode 100644 Bot/Scripts/Airdrop.fsx create mode 100644 Bot/Scripts/GetWhitelisted.fsx create mode 100644 Bot/Scripts/TransferNFT.fsx diff --git a/Airdrop.fsx b/Airdrop.fsx new file mode 100644 index 0000000..44c17e5 --- /dev/null +++ b/Airdrop.fsx @@ -0,0 +1,179 @@ +#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 _ 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 , 16 ) |> Async.StartChild + + +Console.ReadLine() |> ignore diff --git a/Bot/Scripts/Airdrop.fsx b/Bot/Scripts/Airdrop.fsx new file mode 100644 index 0000000..96b8969 --- /dev/null +++ b/Bot/Scripts/Airdrop.fsx @@ -0,0 +1,188 @@ +#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 ( _ , rpcClientUrl )= devEnv.TryGetValue("RPC_CLIENT") +let ( _ , wssClientUrl )= devEnv.TryGetValue("WSS_CLIENT") + +let keystore = SolanaKeyStoreService() +//let file = File.ReadAllText("/home/joe/.config/solana/devnet.json") +let file = File.ReadAllText("/home/joe/.config/solana/DegenzW7kWzac5zEdTWEuqVasoVMPKd16T3za2j6S3qR.json") +let authority = keystore.RestoreKeystore(file) + +//let rpcClient = ClientFactory.GetClient(Cluster.DevNet) +let rpcClient = ClientFactory.GetClient(rpcClientUrl) + +//let mintAccount = PublicKey("2iS6gcoB5VhiLC4eNB7NdcaLgEHjLrXHYpz7T2JMGBDw") +let mintAccount = PublicKey("4gN1u9LNBnvFERpDU3SrMusMJ1gKLbE9XB6EYraQkWTf") +//let ataAccount = PublicKey("CRa7GCMUaB4np32XoTD2sEyCXFVfYKKF4JRPkQTwV3EY") +let sourceAtaAccount = PublicKey("7CB459UJC3qtTYJzfC8bFNvdycQGXTPzKoEHxys8Ytfs") +//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) + } + +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( + sourceAtaAccount, + associatedTokenAccount, + amount, + authority.Account)) + .Build(authority.Account); + async { +// let streamingClient = ClientFactory.GetStreamingClient(Cluster.DevNet) + let streamingClient = ClientFactory.GetStreamingClient(wssClientUrl) + do! streamingClient.ConnectAsync() |> Async.AwaitTask + let blockHash = rpcClient.GetLatestBlockHash() + let! amount = getTokenCountToDrop wallet |> Async.AwaitTask + log $"Dropping {amount} tokens" + let tx = buildTransaction blockHash.Result.Value amount wallet + let! tx = rpcClient.SendTransactionAsync(tx) |> Async.AwaitTask + if tx.ErrorData <> null then + let! _ = updateError wallet (tx.ErrorData.Logs |> String.concat "\n") |> Async.AwaitTask + () + 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 |> Async.AwaitTask + log $"Odd Transaction Error" + return () + else + log $"Successful, now waiting for RPC" + let! _ = updatePending wallet tx.Result |> Async.AwaitTask + 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, check the tx id" + updatePendingError wallet msg tx.Result |> Async.AwaitTask |> Async.Ignore |> Async.Start) |> Async.AwaitTask + return () + } + +let targetWallets = + devConnStr + |> Sql.connect + |> Sql.query """ + SELECT wallet_address FROM crypto WHERE status = 'Ready' AND total_dropped = 0; + """ + |> Sql.execute (fun reader -> reader.string "wallet_address") + +printfn $"Got target wallets: {targetWallets.Length}" + +let asyncs = + targetWallets + |> List.map executeDrop + |> List.chunkBySize 5 + +async { + for a in asyncs do + let! _ = Async.Parallel a + do! Async.Sleep 500 +} |> Async.Start + + +Console.ReadLine() |> ignore + diff --git a/Bot/Scripts/GetWhitelisted.fsx b/Bot/Scripts/GetWhitelisted.fsx new file mode 100644 index 0000000..0014d55 --- /dev/null +++ b/Bot/Scripts/GetWhitelisted.fsx @@ -0,0 +1,148 @@ +#load "/home/joe/Development/DegenzGame/.paket/load/net6.0/main.group.fsx";; + +open Npgsql.FSharp +open DSharpPlus +open DSharpPlus.Entities +open Solnet.Programs +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 firstDrop = + users + |> Seq.filter (fun u -> u.Roles |> Seq.exists (fun role -> + role.Id = 978229149569286174uL)) + |> Seq.distinct + |> Seq.toList + +printfn $"{firstDrop.Length}" + +let changeStatus ids = + devConnStr + |> Sql.connect + |> Sql.parameters [ "ids" , Sql.stringArray ids ] + |> Sql.query """ + UPDATE crypto SET status = 'Ready' WHERE discord_id = ANY (ARRAY[@ids]::varchar[]) AND status = 'Nothing' + """ + |> Sql.executeNonQuery + +changeStatus (firstDrop |> List.map (fun m -> string m.Id) |> List.toArray) + +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 (discord_id) DO + UPDATE SET wallet_address = @wallet, has_wl = @wl , has_og = @og WHERE crypto.discord_id = @did AND crypto.status = 'Nothing'; + """ + |> Sql.executeNonQuery + +let updateUserWithWallet wallet wl og = + devConnStr + |> Sql.connect + |> Sql.parameters [ "wallet" , Sql.string wallet ; "wl" , Sql.bool wl ; "og" , Sql.bool og ] + |> Sql.query """ + UPDATE crypto SET has_wl = @wl, has_og = @og WHERE crypto.wallet_address = @wallet AND crypto.status = 'Nothing'; + """ + |> Sql.executeNonQuery + +let prune ids = + devConnStr + |> Sql.connect + |> Sql.parameters [ "ids" , Sql.stringArray ids ] + |> Sql.query """ + DELETE FROM crypto WHERE discord_id = ANY (ARRAY[@ids]::varchar[]) AND status = 'Nothing' + """ + |> Sql.executeNonQuery + +let getExisting () = + devConnStr + |> Sql.connect + |> Sql.query """ + SELECT discord_id, display_name, wallet_address FROM crypto + """ + |> Sql.execute (fun reader -> {| Id = reader.string "discord_id" |> uint64 ; Name = reader.string "display_name" ; Wallet = reader.string "wallet_address" |}) + +let walletExists wallet = + let list = + devConnStr + |> Sql.connect + |> Sql.parameters [ "wallet" , Sql.string wallet ] + |> Sql.query """ + SELECT wallet_address FROM crypto WHERE wallet_address = @wallet + """ + |> Sql.execute (fun reader -> reader.string "wallet_address") + not list.IsEmpty + +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 + if walletExists wa.Wallet then + let _ = updateUserWithWallet wa.Wallet hasWL hasOG + () + else + let _ = insert wa.Id wa.Name wa.Wallet hasWL hasOG + () + with ex -> printfn $"Huh? {ex.Message}" + () + () + let existing = getExisting () + let usersToFilter = + existing |> List.filter (fun wa -> (whitelisted |> List.exists (fun wl -> wl.Id = wa.Id)) |> not) + let toPrune = usersToFilter |> List.map (fun u -> u.Id |> string) |> List.toArray + let _ = prune toPrune + if toPrune.Length > 0 then + printfn $"Pruning {toPrune.Length} users: {toPrune}" + let existing = getExisting () + printfn $"Total Users: {existing.Length}" + +} |> Async.RunSynchronously + +bot.DisconnectAsync() |> Async.AwaitTask |> Async.RunSynchronously \ No newline at end of file diff --git a/Bot/Scripts/TransferNFT.fsx b/Bot/Scripts/TransferNFT.fsx new file mode 100644 index 0000000..900c907 --- /dev/null +++ b/Bot/Scripts/TransferNFT.fsx @@ -0,0 +1,96 @@ +#load "/home/joe/Development/DegenzGame/.paket/load/net6.0/main.group.fsx";; + +open Npgsql.FSharp +open dotenv.net +open System.IO +open System.Diagnostics +open System.Threading.Tasks + +let prodEnv = DotEnv.Read(DotEnvOptions(envFilePaths = [ "./.prod.env" ])) +let ( _ , prodConnStr )= prodEnv.TryGetValue("DATABASE_URL") + +type NftTransfer = { + Id : uint64 + Name : string + NftName : string + NftAddress : string + UserWallet : string +} + +printfn "Getting Wallet Addresses:" + +let pending = + prodConnStr + |> Sql.connect + |> Sql.query """ + SELECT u.id, u.display_name, token.name, token.address, mpa.tx_wallet FROM gen_one_token token + JOIN mint_pass_assignment mpa on token.id = mpa.token_id + JOIN "user" u on mpa.recipient_id = u.id + WHERE mpa.fulfilled = false AND token.locked = false + ORDER BY mpa.created_at; + """ + |> Sql.execute (fun reader -> + { Id = reader.string "id" |> uint64 + Name = reader.string "display_name" + NftName = reader.string "name" + NftAddress = reader.string "address" + UserWallet = reader.string "tx_wallet" }) + + +let testSample = [ pending |> List.last ] + +type CommandResult = { + ExitCode: int; + StandardOutput: string; + StandardError: string +} + +let executeCommand executable args = + async { + let startInfo = ProcessStartInfo() + startInfo.FileName <- executable + + for a in args do + startInfo.ArgumentList.Add(a) + + startInfo.RedirectStandardOutput <- true + startInfo.RedirectStandardError <- true + startInfo.UseShellExecute <- false + startInfo.CreateNoWindow <- true + use p = new Process() + p.StartInfo <- startInfo + p.Start() |> ignore + + let outTask = + Task.WhenAll( + [| p.StandardOutput.ReadToEndAsync() + p.StandardError.ReadToEndAsync() |] + ) + + do! p.WaitForExitAsync() |> Async.AwaitTask + let! out = outTask |> Async.AwaitTask + + return + { ExitCode = p.ExitCode + StandardOutput = out.[0] + StandardError = out.[1] } + } +let executeShellCommand command = + executeCommand "/usr/bin/env" [ "-S"; "bash"; "-c"; command ] + + +let transfer (nft : NftTransfer) = + printfn $"Transferring {nft.NftName} to {nft.Name} " + let result = + $"spl-token transfer --allow-unfunded-recipient --fund-recipient {nft.NftAddress} 1 {nft.UserWallet}" + |> executeShellCommand + |> Async.RunSynchronously + if result.ExitCode = 0 then + let user = $"{nft.Id} - {nft.Name} - {nft.NftName}" + let tx = result.StandardOutput + File.AppendAllLines("/home/joe/Downloads/transactions", [ user ; tx ]) + else + printfn $"{result.StandardError}" + +for nft in pending do + transfer nft \ No newline at end of file