feat(gateway): wire the wallet.order edge call for the direct rail

Add the WalletOrderRequest / WalletOrderResponse FlatBuffers messages and the
wallet.order Connect op: the gateway decodes the order request, forwards it to
the backend POST /wallet/order and returns the created order id plus the
provider launch URL the client opens. Regenerate the committed Go and TS
FlatBuffers code.
This commit is contained in:
Ilia Denisov
2026-07-09 17:30:39 +02:00
parent a66a5bfa08
commit 2a6dc5a304
9 changed files with 297 additions and 0 deletions
+18
View File
@@ -396,6 +396,24 @@ func (c *Client) WalletBuy(ctx context.Context, userID, productID string) (Walle
return out, err return out, err
} }
// walletOrderBody is the POST body of an order: the chip pack to fund.
type walletOrderBody struct {
ProductID string `json:"product_id"`
}
// WalletOrderResp is a created order: its id and the provider launch URL the client opens.
type WalletOrderResp struct {
OrderID string `json:"order_id"`
RedirectURL string `json:"redirect_url"`
}
// WalletOrder opens a pending order to fund a chip pack and returns the provider launch URL.
func (c *Client) WalletOrder(ctx context.Context, userID, productID string) (WalletOrderResp, error) {
var out WalletOrderResp
err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/order", userID, "", walletOrderBody{ProductID: productID}, &out)
return out, err
}
// CatalogAtomResp is one atom line of a storefront product: the value type it grants and quantity. // CatalogAtomResp is one atom line of a storefront product: the value type it grants and quantity.
type CatalogAtomResp struct { type CatalogAtomResp struct {
AtomType string `json:"atom_type"` AtomType string `json:"atom_type"`
+13
View File
@@ -37,6 +37,19 @@ func encodeAck(ok bool) []byte {
return b.FinishedBytes() return b.FinishedBytes()
} }
// encodeWalletOrder builds a WalletOrderResponse payload: the created order id and the provider
// launch URL the client opens.
func encodeWalletOrder(o backendclient.WalletOrderResp) []byte {
b := flatbuffers.NewBuilder(128)
oid := b.CreateString(o.OrderID)
url := b.CreateString(o.RedirectURL)
fb.WalletOrderResponseStart(b)
fb.WalletOrderResponseAddOrderId(b, oid)
fb.WalletOrderResponseAddRedirectUrl(b, url)
b.Finish(fb.WalletOrderResponseEnd(b))
return b.FinishedBytes()
}
// encodeDeleteRequestResult builds an AccountDeleteRequestResult payload reporting which // encodeDeleteRequestResult builds an AccountDeleteRequestResult payload reporting which
// deletion step-up the account uses ("email" | "phrase"). // deletion step-up the account uses ("email" | "phrase").
func encodeDeleteRequestResult(method string) []byte { func encodeDeleteRequestResult(method string) []byte {
+13
View File
@@ -55,6 +55,7 @@ const (
MsgWalletGet = "wallet.get" MsgWalletGet = "wallet.get"
MsgWalletCatalog = "wallet.catalog" MsgWalletCatalog = "wallet.catalog"
MsgWalletBuy = "wallet.buy" MsgWalletBuy = "wallet.buy"
MsgWalletOrder = "wallet.order"
) )
// Request is one decoded Execute call. // Request is one decoded Execute call.
@@ -111,6 +112,7 @@ func NewRegistry(backend *backendclient.Client, tg TelegramValidator, opts ...Op
r.ops[MsgWalletGet] = Op{Handler: walletHandler(backend), Auth: true} r.ops[MsgWalletGet] = Op{Handler: walletHandler(backend), Auth: true}
r.ops[MsgWalletCatalog] = Op{Handler: walletCatalogHandler(backend), Auth: true} r.ops[MsgWalletCatalog] = Op{Handler: walletCatalogHandler(backend), Auth: true}
r.ops[MsgWalletBuy] = Op{Handler: walletBuyHandler(backend), Auth: true} r.ops[MsgWalletBuy] = Op{Handler: walletBuyHandler(backend), Auth: true}
r.ops[MsgWalletOrder] = Op{Handler: walletOrderHandler(backend), Auth: true}
r.ops[MsgBlockStatus] = Op{Handler: blockStatusHandler(backend), Auth: true} r.ops[MsgBlockStatus] = Op{Handler: blockStatusHandler(backend), Auth: true}
r.ops[MsgGameSubmitPlay] = Op{Handler: submitPlayHandler(backend), Auth: true} r.ops[MsgGameSubmitPlay] = Op{Handler: submitPlayHandler(backend), Auth: true}
r.ops[MsgGameState] = Op{Handler: gameStateHandler(backend), Auth: true} r.ops[MsgGameState] = Op{Handler: gameStateHandler(backend), Auth: true}
@@ -331,6 +333,17 @@ func walletBuyHandler(backend *backendclient.Client) Handler {
} }
} }
func walletOrderHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsWalletOrderRequest(req.Payload, 0)
o, err := backend.WalletOrder(ctx, req.UserID, string(in.ProductId()))
if err != nil {
return nil, err
}
return encodeWalletOrder(o), nil
}
}
func blockStatusHandler(backend *backendclient.Client) Handler { func blockStatusHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) { return func(ctx context.Context, req Request) ([]byte, error) {
bs, err := backend.BlockStatus(ctx, req.UserID) bs, err := backend.BlockStatus(ctx, req.UserID)
+12
View File
@@ -873,6 +873,18 @@ table WalletBuyRequest {
product_id:string; product_id:string;
} }
// WalletOrderRequest opens a money order to fund a chip pack: the pack to buy.
table WalletOrderRequest {
product_id:string;
}
// WalletOrderResponse returns the created order id and the provider launch URL the client opens
// (the Robokassa hosted-payment page); chips are credited later, by the verified server callback.
table WalletOrderResponse {
order_id:string;
redirect_url:string;
}
// CatalogAtom is one atom line of a storefront product: the base value type it grants // CatalogAtom is one atom line of a storefront product: the base value type it grants
// ("chips"/"hints"/"noads_days"/"tournament") and how many of it the product carries. // ("chips"/"hints"/"noads_days"/"tournament") and how many of it the product carries.
table CatalogAtom { table CatalogAtom {
+60
View File
@@ -0,0 +1,60 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type WalletOrderRequest struct {
_tab flatbuffers.Table
}
func GetRootAsWalletOrderRequest(buf []byte, offset flatbuffers.UOffsetT) *WalletOrderRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &WalletOrderRequest{}
x.Init(buf, n+offset)
return x
}
func FinishWalletOrderRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsWalletOrderRequest(buf []byte, offset flatbuffers.UOffsetT) *WalletOrderRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &WalletOrderRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedWalletOrderRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *WalletOrderRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *WalletOrderRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *WalletOrderRequest) ProductId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func WalletOrderRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func WalletOrderRequestAddProductId(builder *flatbuffers.Builder, productId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(productId), 0)
}
func WalletOrderRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+71
View File
@@ -0,0 +1,71 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type WalletOrderResponse struct {
_tab flatbuffers.Table
}
func GetRootAsWalletOrderResponse(buf []byte, offset flatbuffers.UOffsetT) *WalletOrderResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &WalletOrderResponse{}
x.Init(buf, n+offset)
return x
}
func FinishWalletOrderResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsWalletOrderResponse(buf []byte, offset flatbuffers.UOffsetT) *WalletOrderResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &WalletOrderResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedWalletOrderResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *WalletOrderResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *WalletOrderResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *WalletOrderResponse) OrderId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *WalletOrderResponse) RedirectUrl() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func WalletOrderResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func WalletOrderResponseAddOrderId(builder *flatbuffers.Builder, orderId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(orderId), 0)
}
func WalletOrderResponseAddRedirectUrl(builder *flatbuffers.Builder, redirectUrl flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(redirectUrl), 0)
}
func WalletOrderResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+2
View File
@@ -86,6 +86,8 @@ export { UpdateProfileRequest } from './scrabblefb/update-profile-request.js';
export { VKLoginRequest } from './scrabblefb/vklogin-request.js'; export { VKLoginRequest } from './scrabblefb/vklogin-request.js';
export { Wallet } from './scrabblefb/wallet.js'; export { Wallet } from './scrabblefb/wallet.js';
export { WalletBuyRequest } from './scrabblefb/wallet-buy-request.js'; export { WalletBuyRequest } from './scrabblefb/wallet-buy-request.js';
export { WalletOrderRequest } from './scrabblefb/wallet-order-request.js';
export { WalletOrderResponse } from './scrabblefb/wallet-order-response.js';
export { WalletSegment } from './scrabblefb/wallet-segment.js'; export { WalletSegment } from './scrabblefb/wallet-segment.js';
export { WordCheckResult } from './scrabblefb/word-check-result.js'; export { WordCheckResult } from './scrabblefb/word-check-result.js';
export { YourTurnEvent } from './scrabblefb/your-turn-event.js'; export { YourTurnEvent } from './scrabblefb/your-turn-event.js';
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class WalletOrderRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):WalletOrderRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsWalletOrderRequest(bb:flatbuffers.ByteBuffer, obj?:WalletOrderRequest):WalletOrderRequest {
return (obj || new WalletOrderRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsWalletOrderRequest(bb:flatbuffers.ByteBuffer, obj?:WalletOrderRequest):WalletOrderRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new WalletOrderRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
productId():string|null
productId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
productId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startWalletOrderRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addProductId(builder:flatbuffers.Builder, productIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, productIdOffset, 0);
}
static endWalletOrderRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createWalletOrderRequest(builder:flatbuffers.Builder, productIdOffset:flatbuffers.Offset):flatbuffers.Offset {
WalletOrderRequest.startWalletOrderRequest(builder);
WalletOrderRequest.addProductId(builder, productIdOffset);
return WalletOrderRequest.endWalletOrderRequest(builder);
}
}
@@ -0,0 +1,60 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class WalletOrderResponse {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):WalletOrderResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsWalletOrderResponse(bb:flatbuffers.ByteBuffer, obj?:WalletOrderResponse):WalletOrderResponse {
return (obj || new WalletOrderResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsWalletOrderResponse(bb:flatbuffers.ByteBuffer, obj?:WalletOrderResponse):WalletOrderResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new WalletOrderResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
orderId():string|null
orderId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
orderId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
redirectUrl():string|null
redirectUrl(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
redirectUrl(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startWalletOrderResponse(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addOrderId(builder:flatbuffers.Builder, orderIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, orderIdOffset, 0);
}
static addRedirectUrl(builder:flatbuffers.Builder, redirectUrlOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, redirectUrlOffset, 0);
}
static endWalletOrderResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createWalletOrderResponse(builder:flatbuffers.Builder, orderIdOffset:flatbuffers.Offset, redirectUrlOffset:flatbuffers.Offset):flatbuffers.Offset {
WalletOrderResponse.startWalletOrderResponse(builder);
WalletOrderResponse.addOrderId(builder, orderIdOffset);
WalletOrderResponse.addRedirectUrl(builder, redirectUrlOffset);
return WalletOrderResponse.endWalletOrderResponse(builder);
}
}