--[[ Rograde — приём оплаты внутри игры Roblox. Куда класть: ServerScriptService (обычный Script, RunContext = Legacy/Server). Что делает: игрок покупает developer product → Roblox вызывает ProcessReceipt → скрипт шлёт подписанный запрос на бэкенд Rograde → сайт начисляет баланс. Перед запуском: 1. В Game Settings → Security включи Allow HTTP Requests. 2. Впиши BACKEND_URL и SECRET (секрет лежит в config.json бэкенда, поле gameHookSecret). 3. Создай developer products и пропиши их id в PRODUCTS ниже И в config.json сайта. --]] local HttpService = game:GetService("HttpService") local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local BACKEND_URL = "http://localhost:3000/api/hook/roblox" -- на проде: https://твой-домен/api/hook/roblox local SECRET = "ВСТАВЬ-gameHookSecret-ИЗ-config.json" -- productId = сколько Robux он стоит local PRODUCTS = { [0] = 100, -- замени 0 на реальный productId -- [123456789] = 400, -- [123456790] = 800, } local RETRY_QUEUE = {} --[[ Подпись HMAC-SHA256 сырого тела. У Roblox нет встроенного HMAC, поэтому считаем сами. ]] local function sha256(msg) local k = { 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2, } local function rrot(x, n) return bit32.bor(bit32.rshift(x, n), bit32.lshift(x, 32 - n)) end local h = {0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19} local len = #msg msg = msg .. "\128" .. string.rep("\0", (55 - len) % 64) .. string.pack(">I8", len * 8) for i = 1, #msg, 64 do local w = {} for j = 0, 15 do w[j + 1] = string.unpack(">I4", msg, i + j * 4) end for j = 17, 64 do local s0 = bit32.bxor(rrot(w[j-15], 7), rrot(w[j-15], 18), bit32.rshift(w[j-15], 3)) local s1 = bit32.bxor(rrot(w[j-2], 17), rrot(w[j-2], 19), bit32.rshift(w[j-2], 10)) w[j] = (w[j-16] + s0 + w[j-7] + s1) % 2^32 end local a,b,c,d,e,f,g,hh = h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8] for j = 1, 64 do local S1 = bit32.bxor(rrot(e, 6), rrot(e, 11), rrot(e, 25)) local ch = bit32.bxor(bit32.band(e, f), bit32.band(bit32.bnot(e), g)) local t1 = (hh + S1 + ch + k[j] + w[j]) % 2^32 local S0 = bit32.bxor(rrot(a, 2), rrot(a, 13), rrot(a, 22)) local maj = bit32.bxor(bit32.band(a, b), bit32.band(a, c), bit32.band(b, c)) local t2 = (S0 + maj) % 2^32 hh, g, f, e, d, c, b, a = g, f, e, (d + t1) % 2^32, c, b, a, (t1 + t2) % 2^32 end h[1]=(h[1]+a)%2^32 h[2]=(h[2]+b)%2^32 h[3]=(h[3]+c)%2^32 h[4]=(h[4]+d)%2^32 h[5]=(h[5]+e)%2^32 h[6]=(h[6]+f)%2^32 h[7]=(h[7]+g)%2^32 h[8]=(h[8]+hh)%2^32 end local out = {} for i = 1, 8 do out[i] = string.pack(">I4", h[i]) end return table.concat(out) end local function hmacSha256Hex(key, message) if #key > 64 then key = sha256(key) end key = key .. string.rep("\0", 64 - #key) local o, i = {}, {} for n = 1, 64 do local b = string.byte(key, n) o[n] = string.char(bit32.bxor(b, 0x5c)) i[n] = string.char(bit32.bxor(b, 0x36)) end local digest = sha256(table.concat(o) .. sha256(table.concat(i) .. message)) return (digest:gsub(".", function(ch) return string.format("%02x", string.byte(ch)) end)) end local function send(payload) local body = HttpService:JSONEncode(payload) local ok, response = pcall(function() return HttpService:RequestAsync({ Url = BACKEND_URL, Method = "POST", Headers = { ["Content-Type"] = "application/json", ["x-rograde-signature"] = hmacSha256Hex(SECRET, body), }, Body = body, }) end) if ok and response and response.Success then return true end warn("[Rograde] бэкенд не принял платёж:", ok and response and response.StatusCode or response) return false end MarketplaceService.ProcessReceipt = function(receiptInfo) local robux = PRODUCTS[receiptInfo.ProductId] if not robux then warn("[Rograde] неизвестный продукт", receiptInfo.ProductId) return Enum.ProductPurchaseDecision.NotProcessedYet end local payload = { receiptId = receiptInfo.PurchaseId, robloxUserId = tostring(receiptInfo.PlayerId), robux = robux, productId = receiptInfo.ProductId, placeId = receiptInfo.PlaceIdWherePurchased, } if send(payload) then local plr = Players:GetPlayerByUserId(receiptInfo.PlayerId) if plr then -- необязательно: подтверждение игроку local msg = Instance.new("Message") msg.Text = ("Rograde: balance topped up by %d R$"):format(robux) msg.Parent = plr:FindFirstChildOfClass("PlayerGui") or plr task.delay(4, function() msg:Destroy() end) end return Enum.ProductPurchaseDecision.PurchaseGranted end -- бэкенд недоступен — кладём в очередь и просим Roblox повторить table.insert(RETRY_QUEUE, payload) return Enum.ProductPurchaseDecision.NotProcessedYet end -- повторные попытки для платежей, которые не дошли task.spawn(function() while true do task.wait(30) for i = #RETRY_QUEUE, 1, -1 do if send(RETRY_QUEUE[i]) then table.remove(RETRY_QUEUE, i) end end end end)