155 lines
3.4 KiB
Plaintext
155 lines
3.4 KiB
Plaintext
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"log"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
walletpb "_gen"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/metadata"
|
|
)
|
|
|
|
const (
|
|
sharedSecret = "super_secret_key"
|
|
serviceName = "game-service"
|
|
)
|
|
|
|
func main() {
|
|
conn, err := grpc.Dial(
|
|
"localhost:50051",
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithUnaryInterceptor(hmacUnaryClientInterceptor(sharedSecret, serviceName)),
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("dial error: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := walletpb.NewWalletServiceClient(conn)
|
|
ctx := context.Background()
|
|
|
|
1. reserve
|
|
reserveResp, err := client.Reserve(ctx, &walletpb.ReserveRequest{
|
|
RequestId: "req-1",
|
|
UserId: "user-1",
|
|
GameId: "slot-42",
|
|
RoundId: "round-1001",
|
|
Amount: 1000, 10.00
|
|
Currency: "EUR",
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("reserve error: %v", err)
|
|
}
|
|
|
|
log.Printf("reserve: tx=%s status=%s available=%d reserved=%d",
|
|
reserveResp.TransactionId,
|
|
reserveResp.Status,
|
|
reserveResp.AvailableBalance,
|
|
reserveResp.ReservedBalance,
|
|
)
|
|
|
|
Тут игра “сыграла”. Допустим пользователь выиграл 25.00
|
|
confirmResp, err := client.Confirm(ctx, &walletpb.ConfirmRequest{
|
|
RequestId: "req-2",
|
|
TransactionId: reserveResp.TransactionId,
|
|
RoundId: "round-1001",
|
|
WinAmount: 2500,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("confirm error: %v", err)
|
|
}
|
|
|
|
log.Printf("confirm: tx=%s status=%s available=%d reserved=%d",
|
|
confirmResp.TransactionId,
|
|
confirmResp.Status,
|
|
confirmResp.AvailableBalance,
|
|
confirmResp.ReservedBalance,
|
|
)
|
|
|
|
txResp, err := client.GetTransaction(ctx, &walletpb.GetTransactionRequest{
|
|
TransactionId: reserveResp.TransactionId,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("get tx error: %v", err)
|
|
}
|
|
|
|
log.Printf("transaction: id=%s status=%s amount=%d win=%d",
|
|
txResp.TransactionId, txResp.Status, txResp.Amount, txResp.WinAmount)
|
|
}
|
|
|
|
---------- HMAC client interceptor ----------
|
|
|
|
func hmacUnaryClientInterceptor(secret, service string) grpc.UnaryClientInterceptor {
|
|
return func(
|
|
ctx context.Context,
|
|
method string,
|
|
req any,
|
|
reply any,
|
|
cc *grpc.ClientConn,
|
|
invoker grpc.UnaryInvoker,
|
|
opts ...grpc.CallOption,
|
|
) error {
|
|
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
|
|
|
md, ok := metadata.FromOutgoingContext(ctx)
|
|
if !ok {
|
|
md = metadata.New(nil)
|
|
} else {
|
|
md = md.Copy()
|
|
}
|
|
|
|
md.Set("x-service-name", service)
|
|
md.Set("x-timestamp", timestamp)
|
|
|
|
payload := buildSigningPayload(service, method, timestamp, md)
|
|
signature := computeHMAC(payload, secret)
|
|
|
|
md.Set("x-signature", signature)
|
|
ctx = metadata.NewOutgoingContext(ctx, md)
|
|
|
|
return invoker(ctx, method, req, reply, cc, opts...)
|
|
}
|
|
}
|
|
|
|
func computeHMAC(payload, secret string) string {
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(payload))
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func buildSigningPayload(serviceName, method, timestamp string, md metadata.MD) string {
|
|
var parts []string
|
|
parts = append(parts,
|
|
"service="+serviceName,
|
|
"method="+method,
|
|
"timestamp="+timestamp,
|
|
)
|
|
|
|
var keys []string
|
|
for k := range md {
|
|
if strings.ToLower(k) == "x-signature" {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(k), ":") {
|
|
continue
|
|
}
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
for _, k := range keys {
|
|
vals := md.Get(k)
|
|
sort.Strings(vals)
|
|
parts = append(parts, k+"="+strings.Join(vals, ","))
|
|
}
|
|
|
|
return strings.Join(parts, "|")
|
|
} |