Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce92780f2f | ||
|
|
0db69bf72b | ||
|
|
7cae807bfa | ||
|
|
2c935143cc | ||
|
|
d908e2bc3f | ||
|
|
027074a13e | ||
|
|
66424d9d6e | ||
|
|
c95f6c642d | ||
|
|
05291c2dbc |
No files matched your search
@@ -125,6 +125,35 @@ let getUserInvites userIds =
|
|||||||
|> Sql.executeAsync (fun read -> {| Username = read.string "display_name" ; DiscordId = read.string "inviter" |> uint64 ; TotalInvites = read.int "total_invites" |})
|
|> Sql.executeAsync (fun read -> {| Username = read.string "display_name" ; DiscordId = read.string "inviter" |> uint64 ; TotalInvites = read.int "total_invites" |})
|
||||||
|> Async.AwaitTask
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
type MerrisItem =
|
||||||
|
| NuuPing = 0
|
||||||
|
| Noodles = 1
|
||||||
|
| Pizza = 2
|
||||||
|
|
||||||
|
let updateMerrisItem itemName amount =
|
||||||
|
let itemId =
|
||||||
|
match itemName with
|
||||||
|
| MerrisItem.Noodles -> "NOODLES"
|
||||||
|
| MerrisItem.Pizza -> "PIZZA"
|
||||||
|
| MerrisItem.NuuPing | _ -> "GRILLED_RAT"
|
||||||
|
|
||||||
|
GuildEnvironment.connectionString
|
||||||
|
|> Sql.connect
|
||||||
|
|> Sql.parameters [ "itemId" , Sql.string itemId ; "amount" , Sql.int amount ]
|
||||||
|
|> Sql.query "UPDATE mart_item SET stock = @amount WHERE id = @itemId"
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
let restockMerris itemName amount (ctx : IDiscordContext) =
|
||||||
|
task {
|
||||||
|
do! Messaging.defer ctx
|
||||||
|
if amount > 0 && amount < 1000 then
|
||||||
|
do! updateMerrisItem itemName amount |> Async.Ignore
|
||||||
|
do! Messaging.sendFollowUpMessage ctx $"Restocked {itemName} by {amount}"
|
||||||
|
else
|
||||||
|
do! Messaging.sendFollowUpMessage ctx $"Provide a restock amount between 0 and 1000"
|
||||||
|
} :> Task
|
||||||
|
|
||||||
let getUsersFromMessageReactions (channel : DiscordChannel) (message : string) (ctx : IDiscordContext) =
|
let getUsersFromMessageReactions (channel : DiscordChannel) (message : string) (ctx : IDiscordContext) =
|
||||||
task {
|
task {
|
||||||
do! Messaging.defer ctx
|
do! Messaging.defer ctx
|
||||||
@@ -284,6 +313,12 @@ type AdminBot() =
|
|||||||
member this.GetRaffleWinners (ctx : InteractionContext, [<Option("count", "How many winners to pick")>] count : int64) =
|
member this.GetRaffleWinners (ctx : InteractionContext, [<Option("count", "How many winners to pick")>] count : int64) =
|
||||||
enforceAdmin (DiscordInteractionContext ctx) (getRaffleWinners count)
|
enforceAdmin (DiscordInteractionContext ctx) (getRaffleWinners count)
|
||||||
|
|
||||||
|
[<SlashCommand("restock-merris", "Restock Merris Mart Item")>]
|
||||||
|
member this.ResstockMerris (ctx : InteractionContext,
|
||||||
|
[<Option("item-id", "Which item to restock")>] itemId : MerrisItem,
|
||||||
|
[<Option("amount", "How much to restock by")>] amount : int64) =
|
||||||
|
restockMerris itemId (int amount) (DiscordInteractionContext ctx)
|
||||||
|
|
||||||
// [<SlashCommand("admin-raffles-toggle", "Toggle availability of an item")>]
|
// [<SlashCommand("admin-raffles-toggle", "Toggle availability of an item")>]
|
||||||
// member this.ToggleRaffle (ctx : InteractionContext, [<Option("enabled", "Enable or Disable?")>] enable : EnableDisable) =
|
// member this.ToggleRaffle (ctx : InteractionContext, [<Option("enabled", "Enable or Disable?")>] enable : EnableDisable) =
|
||||||
// enforceAdmin (DiscordInteractionContext ctx) (toggleRaffleAvailability (enable = EnableDisable.Enable))
|
// enforceAdmin (DiscordInteractionContext ctx) (toggleRaffleAvailability (enable = EnableDisable.Enable))
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ let roleWhitelistPending = getId "ROLE_WHITELIST_PENDING"
|
|||||||
let roleWhiteOGPending = getId "ROLE_WHITEOG_PENDING"
|
let roleWhiteOGPending = getId "ROLE_WHITEOG_PENDING"
|
||||||
let roleWhitelist = getId "ROLE_WHITELIST"
|
let roleWhitelist = getId "ROLE_WHITELIST"
|
||||||
let roleWhiteOG = getId "ROLE_WHITEOG"
|
let roleWhiteOG = getId "ROLE_WHITEOG"
|
||||||
|
// let roleMod = getId "ROLE_MOD"
|
||||||
let roleAdmin = getId "ROLE_ADMIN"
|
let roleAdmin = getId "ROLE_ADMIN"
|
||||||
let roleMagicEden = getId "ROLE_MAGICEDEN"
|
let roleMagicEden = getId "ROLE_MAGICEDEN"
|
||||||
let roleRecruiter1x = getId "ROLE_RECRUITER_1X"
|
let roleRecruiter1x = getId "ROLE_RECRUITER_1X"
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
#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
|
|
||||||
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
:headers = <<
|
||||||
|
Content-Type: application/json
|
||||||
|
X-API-Key: 1a2ff166-8985-47f0-8e5a-a7c6fb321a3f
|
||||||
|
#
|
||||||
|
|
||||||
|
# Get balance
|
||||||
|
GET https://degenz-currency-api-prod-jv56z.ondigitalocean.app/user/90588624566886400/balance
|
||||||
|
:headers
|
||||||
|
|
||||||
|
# Increment balance
|
||||||
|
PATCH https://degenz-currency-api-prod-jv56z.ondigitalocean.app/user/90588624566886400/balance/deposit
|
||||||
|
:headers
|
||||||
|
{ "amount" : 10 }
|
||||||
|
|
||||||
|
# Decrement balance
|
||||||
|
PATCH https://degenz-currency-api-prod-jv56z.ondigitalocean.app/user/90588624566886400/balance/withdraw
|
||||||
|
:headers
|
||||||
|
{ "amount" : 100 }
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# DegenzGame
|
||||||
|
|
||||||
|
A multiplayer Discord gaming platform built with F# and the Giraffe web framework, featuring interactive games with player progression and an in-game economy.
|
||||||
|
|
||||||
|
## Screenshots & Game Assets
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Technologies
|
||||||
|
|
||||||
|
- **F# (.NET 6.0)**: Primary programming language for all game logic and bot functionality
|
||||||
|
- **Giraffe**: F# web framework used for the Currency API microservice
|
||||||
|
- **DSharpPlus**: Discord API library for bot interactions and slash commands
|
||||||
|
- **PostgreSQL**: Database with Npgsql.FSharp for type-safe operations
|
||||||
|
- **Docker**: Containerized deployment
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
The solution consists of two main projects:
|
||||||
|
|
||||||
|
- **Bot** (`Bot.fsproj`): Main Discord bot application containing all game implementations
|
||||||
|
- **CurrencyAPI** (`CurrencyAPI.fsproj`): HTTP API microservice for currency balance management
|
||||||
|
|
||||||
|
## Game System
|
||||||
|
|
||||||
|
### Available Games
|
||||||
|
|
||||||
|
#### HackerBattle
|
||||||
|
Cyberpunk-themed combat game where players:
|
||||||
|
- Use hack items (VIRUS, REMOTE, WORM) to attack other players and steal GBT currency
|
||||||
|
- Deploy shields (FIREWALL, ENCRYPTION, CYPHER) for defense
|
||||||
|
- Items have cooldown periods and class-based effectiveness
|
||||||
|
- Combat resolution based on item power vs resistance stats
|
||||||
|
- Includes comprehensive validation (can't attack yourself, cooldown checks, etc.)
|
||||||
|
|
||||||
|
#### SlotMachine
|
||||||
|
Casino-style slot machine featuring:
|
||||||
|
- Custom symbols: BigBrother, Eye, Obey, AnonMask, Rat, Ramen, Sushi, Pizza
|
||||||
|
- Configurable reel probabilities per symbol
|
||||||
|
- Prize table with money rewards and jackpot system
|
||||||
|
- Integration with GBT currency system
|
||||||
|
|
||||||
|
#### Thief Game
|
||||||
|
Player-vs-player stealing mechanics:
|
||||||
|
- 1-minute cooldown for thieves, 1-hour recovery for victims
|
||||||
|
- Success chance calculation based on player stats
|
||||||
|
- Payout formula: `defenderBank * 0.1 * (1.0 - chance)` with random bonus
|
||||||
|
- Two outcomes: Success (steal GBT) or WentToPrison (imprisoned status)
|
||||||
|
|
||||||
|
#### Rock Paper Scissors
|
||||||
|
Classic game with Discord UI integration and animated GIF results
|
||||||
|
|
||||||
|
#### Store System
|
||||||
|
In-game marketplace with:
|
||||||
|
- Fixed-price item purchasing with GBT currency
|
||||||
|
- Stock management (limited/unlimited items)
|
||||||
|
- Role-based access control and invite requirements
|
||||||
|
- Timed sales with Unix timestamp endpoints
|
||||||
|
- Stack limits for certain items
|
||||||
|
|
||||||
|
#### Trainer System
|
||||||
|
Tutorial/onboarding system:
|
||||||
|
- Grants "Trainee" role to new players
|
||||||
|
- Multi-step tutorial teaching shield and hack mechanics
|
||||||
|
- Provides starting items (REMOTE hack, FIREWALL shield)
|
||||||
|
- Simulated combat against "Sensei" bot
|
||||||
|
|
||||||
|
### Player Progression
|
||||||
|
|
||||||
|
#### Stats System (`GameTypes.fs:41-55`)
|
||||||
|
Four core attributes with time-based decay:
|
||||||
|
- **Strength**: Combat effectiveness
|
||||||
|
- **Focus**: Precision and accuracy
|
||||||
|
- **Charisma**: Social interactions
|
||||||
|
- **Luck**: Random event outcomes
|
||||||
|
|
||||||
|
Each stat features:
|
||||||
|
- Base range 0-100 with item-based modifications
|
||||||
|
- Decay rate of 2.09 (100 to 0 in ~48 hours)
|
||||||
|
- Item effects: Min/Max bounds, Add bonuses, RateMultiplier
|
||||||
|
|
||||||
|
## Discord Integration
|
||||||
|
|
||||||
|
### Multi-Bot Architecture (`Bot.fs:53-59`)
|
||||||
|
The system uses multiple specialized Discord bot clients:
|
||||||
|
- **hackerBattleBot**: HackerBattle game interactions
|
||||||
|
- **storeBot**: Store transactions and interactions
|
||||||
|
- **inviterBot**: Member recruitment and invite tracking
|
||||||
|
- **slotsBot**: Slot machine operations
|
||||||
|
- **adminBot**: Administrative functions
|
||||||
|
|
||||||
|
### Event Handling
|
||||||
|
- Component interactions (buttons, dropdowns)
|
||||||
|
- Slash command registration per guild
|
||||||
|
- Message creation events
|
||||||
|
- Guild member join/update tracking
|
||||||
|
- Real-time game state management
|
||||||
|
|
||||||
|
## Database Design
|
||||||
|
|
||||||
|
### PostgreSQL Integration (`DbService.fs`)
|
||||||
|
Type-safe database operations using Npgsql.FSharp:
|
||||||
|
|
||||||
|
- **User table**: Discord IDs, display names, GBT balances, wallet addresses
|
||||||
|
- **Item system**: Database-driven item definitions with custom composite types
|
||||||
|
- **Event tracking**: Player actions, cooldowns, game history with proper typing
|
||||||
|
- **Custom types**: `StatMod` composite type for database item modifiers
|
||||||
|
|
||||||
|
## Currency API (Giraffe)
|
||||||
|
|
||||||
|
### Endpoints (`Currency.fs`)
|
||||||
|
- **GET `/user/{id}/balance`**: Retrieve player's GBT balance
|
||||||
|
- **PATCH `/user/{id}/balance/withdraw`**: Deduct GBT (with insufficient funds validation)
|
||||||
|
- **PATCH `/user/{id}/balance/deposit`**: Add GBT to player account
|
||||||
|
- **API Key Authentication**: `X-API-Key` header validation
|
||||||
|
- **Error Handling**: Comprehensive error responses and logging
|
||||||
|
|
||||||
|
## Player Economy
|
||||||
|
|
||||||
|
The game features a balanced virtual economy:
|
||||||
|
- **GBT Currency**: Primary medium of exchange earned through gameplay
|
||||||
|
- **Fixed Pricing**: Items have set buy/sell prices (no dynamic market)
|
||||||
|
- **Cooldown Systems**: Rate limiting prevents exploitation
|
||||||
|
- **Risk/Reward Balance**: Thief game risk vs potential reward
|
||||||
|
- **Player Progression**: Long-term engagement through stat decay requiring active play
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- .NET 6.0 SDK
|
||||||
|
- PostgreSQL database
|
||||||
|
- Discord application with multiple bot tokens
|
||||||
|
- Paket for F# package management
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
Environment variables required:
|
||||||
|
- `DATABASE_URL`: PostgreSQL connection string
|
||||||
|
- `API_KEY`: Currency API authentication key
|
||||||
|
- Multiple Discord bot tokens for specialized bots
|
||||||
|
|
||||||
|
### Running the Application
|
||||||
|
```bash
|
||||||
|
# Restore packages
|
||||||
|
dotnet paket restore
|
||||||
|
|
||||||
|
# Build the solution
|
||||||
|
dotnet build
|
||||||
|
|
||||||
|
# Run the Discord bot
|
||||||
|
dotnet run --project Bot
|
||||||
|
|
||||||
|
# Run the Currency API
|
||||||
|
dotnet run --project CurrencyAPI
|
||||||
|
```
|
||||||
+3
-3
@@ -5,9 +5,9 @@ framework: net6.0, netstandard2.0, netstandard2.1
|
|||||||
|
|
||||||
nuget FSharp.Core >= 6.0.0
|
nuget FSharp.Core >= 6.0.0
|
||||||
|
|
||||||
nuget DSharpPlus >= 4.3.0-nightly-01135
|
nuget DSharpPlus >= 4.3.0-nightly-01160
|
||||||
nuget DSharpPlus.Interactivity >= 4.3.0-nightly-01135
|
nuget DSharpPlus.Interactivity >= 4.3.0-nightly-01160
|
||||||
nuget DSharpPlus.SlashCommands >= 4.3.0-nightly-01135
|
nuget DSharpPlus.SlashCommands >= 4.3.0-nightly-01160
|
||||||
nuget FSharp.Data
|
nuget FSharp.Data
|
||||||
nuget FsToolkit.ErrorHandling
|
nuget FsToolkit.ErrorHandling
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user