update payment ui and change api route path
This commit is contained in:
@@ -17,6 +17,7 @@ type Orders struct {
|
||||
ActualAmount float64 `gorm:"column:actual_amount" json:"actual_amount"`
|
||||
ReceiveAddress string `gorm:"column:receive_address" json:"receive_address"`
|
||||
Token string `gorm:"column:token" json:"token"`
|
||||
Network string `gorm:"column:network" json:"network"`
|
||||
Status int `gorm:"column:status;default:1" json:"status"`
|
||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"`
|
||||
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"`
|
||||
|
||||
@@ -6,6 +6,7 @@ const (
|
||||
)
|
||||
|
||||
type WalletAddress struct {
|
||||
Network string `gorm:"column:network;uniqueIndex:wallet_address_network_address_uindex" json:"network"`
|
||||
Address string `gorm:"column:address;uniqueIndex:wallet_address_address_uindex" json:"address"`
|
||||
Status int64 `gorm:"column:status;default:1" json:"status"`
|
||||
BaseModel
|
||||
|
||||
@@ -5,8 +5,9 @@ import "github.com/gookit/validate"
|
||||
// CreateTransactionRequest 创建交易请求
|
||||
type CreateTransactionRequest struct {
|
||||
OrderId string `json:"order_id" validate:"required|maxLen:32"`
|
||||
Currency string `json:"currency" validate:"required"`
|
||||
Token string `json:"token" validate:"required"`
|
||||
Currency string `json:"currency" validate:"required"` // 法币 如:cny
|
||||
Token string `json:"token" validate:"required"` // 币种 如:usdt
|
||||
Network string `json:"network" validate:"required"` // 网络 如:TRON
|
||||
Amount float64 `json:"amount" validate:"required|isFloat|gt:0.01"`
|
||||
NotifyUrl string `json:"notify_url" validate:"required"`
|
||||
Signature string `json:"signature" validate:"required"`
|
||||
@@ -18,6 +19,7 @@ func (r CreateTransactionRequest) Translates() map[string]string {
|
||||
"OrderId": "订单号",
|
||||
"Currency": "货币",
|
||||
"Token": "币种",
|
||||
"Network": "网络",
|
||||
"Amount": "支付金额",
|
||||
"NotifyUrl": "异步回调网址",
|
||||
"Signature": "签名",
|
||||
@@ -29,6 +31,7 @@ type OrderProcessingRequest struct {
|
||||
ReceiveAddress string
|
||||
Currency string
|
||||
Token string
|
||||
Network string
|
||||
Amount float64
|
||||
TradeId string
|
||||
BlockTransactionId string
|
||||
|
||||
@@ -2,11 +2,15 @@ package response
|
||||
|
||||
type CheckoutCounterResponse struct {
|
||||
TradeId string `json:"trade_id"` // epusdt订单号
|
||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
ReceiveAddress string `json:"receive_address"` // 收款钱包地址
|
||||
Amount float64 `json:"amount"` // 订单金额,保留4位小数 法币金额
|
||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数 加密货币金额
|
||||
Token string `json:"token"` // 所属币种 TRX USDT......
|
||||
Currency string `json:"currency"` // 法币币种 CNY USD ...
|
||||
ReceiveAddress string `json:"receive_address"` // 收款钱包地址
|
||||
Network string `json:"network"` // 网络 TRON ETH ...
|
||||
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||
RedirectUrl string `json:"redirect_url"`
|
||||
CreatedAt int64 `json:"created_at"` // 订单创建时间 时间戳
|
||||
}
|
||||
|
||||
type CheckStatusResponse struct {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/assimon/luuu/model/response"
|
||||
)
|
||||
|
||||
var ErrOrder = errors.New("不存在待支付订单或已过期")
|
||||
|
||||
// GetCheckoutCounterByTradeId returns checkout info for a pending order.
|
||||
func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterResponse, error) {
|
||||
orderInfo, err := data.GetOrderInfoByTradeId(tradeId)
|
||||
@@ -16,16 +18,20 @@ func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterRespo
|
||||
return nil, err
|
||||
}
|
||||
if orderInfo.ID <= 0 || orderInfo.Status != mdb.StatusWaitPay {
|
||||
return nil, errors.New("不存在待支付订单或已过期")
|
||||
return nil, ErrOrder
|
||||
}
|
||||
|
||||
resp := &response.CheckoutCounterResponse{
|
||||
TradeId: orderInfo.TradeId,
|
||||
Amount: orderInfo.Amount,
|
||||
ActualAmount: orderInfo.ActualAmount,
|
||||
ReceiveAddress: orderInfo.ReceiveAddress,
|
||||
Token: orderInfo.Token,
|
||||
Currency: orderInfo.Currency,
|
||||
ReceiveAddress: orderInfo.ReceiveAddress,
|
||||
Network: orderInfo.Network,
|
||||
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||
RedirectUrl: orderInfo.RedirectUrl,
|
||||
CreatedAt: orderInfo.CreatedAt.TimestampMilli(),
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
)
|
||||
|
||||
func SolCallBack(address string) {}
|
||||
|
||||
const (
|
||||
// Mint token
|
||||
USDT_Mint = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
||||
USDC_Mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenProgramID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
Token2022ProgramID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
||||
)
|
||||
|
||||
func FindATAAddress(owner, mint string) (string, error) {
|
||||
ownerPubKey, err := solana.PublicKeyFromBase58(owner)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid owner public key: %w", err)
|
||||
}
|
||||
|
||||
mintPubKey, err := solana.PublicKeyFromBase58(mint)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid mint public key: %w", err)
|
||||
}
|
||||
|
||||
ata, _, err := solana.FindAssociatedTokenAddress(ownerPubKey, mintPubKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("find associated token address failed: %w", err)
|
||||
}
|
||||
|
||||
return ata.String(), nil
|
||||
}
|
||||
|
||||
const (
|
||||
InstructionTransfer = 3
|
||||
InstructionTransferChecked = 12
|
||||
)
|
||||
|
||||
type TransferInfo struct {
|
||||
ProgramID string
|
||||
Type string
|
||||
|
||||
Source string
|
||||
Destination string
|
||||
Mint string
|
||||
Authority string
|
||||
|
||||
Amount uint64
|
||||
Decimals *uint8
|
||||
}
|
||||
|
||||
func isTokenProgram(programID solana.PublicKey) bool {
|
||||
p := programID.String()
|
||||
return p == TokenProgramID || p == Token2022ProgramID
|
||||
}
|
||||
|
||||
func DecodeTransfer(data []byte, accountKeys []solana.PublicKey, accountIndexes []uint16, programID solana.PublicKey) (*TransferInfo, error) {
|
||||
if len(data) < 9 {
|
||||
return nil, fmt.Errorf("invalid transfer data length: %d", len(data))
|
||||
}
|
||||
|
||||
if data[0] != InstructionTransfer {
|
||||
return nil, fmt.Errorf("not transfer instruction, got: %d", data[0])
|
||||
}
|
||||
|
||||
if len(accountIndexes) < 3 {
|
||||
return nil, fmt.Errorf("transfer accounts not enough: %d", len(accountIndexes))
|
||||
}
|
||||
|
||||
amount := binary.LittleEndian.Uint64(data[1:9])
|
||||
|
||||
sourceIdx := accountIndexes[0]
|
||||
destIdx := accountIndexes[1]
|
||||
authIdx := accountIndexes[2]
|
||||
|
||||
if int(sourceIdx) >= len(accountKeys) || int(destIdx) >= len(accountKeys) || int(authIdx) >= len(accountKeys) {
|
||||
return nil, errors.New("account index out of range")
|
||||
}
|
||||
|
||||
info := &TransferInfo{
|
||||
ProgramID: programID.String(),
|
||||
Type: "transfer",
|
||||
Source: accountKeys[sourceIdx].String(),
|
||||
Destination: accountKeys[destIdx].String(),
|
||||
Authority: accountKeys[authIdx].String(),
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type TransferCheckedInfo struct {
|
||||
ProgramID string
|
||||
Type string
|
||||
|
||||
Source string
|
||||
Destination string
|
||||
Mint string
|
||||
Authority string
|
||||
|
||||
Amount uint64
|
||||
Decimals uint8
|
||||
}
|
||||
|
||||
func DecodeTransferCheck(data []byte, accountKeys []solana.PublicKey, accountIndexes []uint16, programID solana.PublicKey) (*TransferInfo, error) {
|
||||
if len(data) < 10 {
|
||||
return nil, fmt.Errorf("invalid transferChecked data length: %d", len(data))
|
||||
}
|
||||
|
||||
if data[0] != InstructionTransferChecked {
|
||||
return nil, fmt.Errorf("not transferChecked instruction, got: %d", data[0])
|
||||
}
|
||||
|
||||
if len(accountIndexes) < 4 {
|
||||
return nil, fmt.Errorf("transferChecked accounts not enough: %d", len(accountIndexes))
|
||||
}
|
||||
|
||||
amount := binary.LittleEndian.Uint64(data[1:9])
|
||||
decimals := uint8(data[9])
|
||||
|
||||
sourceIdx := accountIndexes[0]
|
||||
mintIdx := accountIndexes[1]
|
||||
destIdx := accountIndexes[2]
|
||||
authIdx := accountIndexes[3]
|
||||
|
||||
if int(sourceIdx) >= len(accountKeys) ||
|
||||
int(mintIdx) >= len(accountKeys) ||
|
||||
int(destIdx) >= len(accountKeys) ||
|
||||
int(authIdx) >= len(accountKeys) {
|
||||
return nil, errors.New("account index out of range")
|
||||
}
|
||||
|
||||
info := &TransferInfo{
|
||||
ProgramID: programID.String(),
|
||||
Type: "transferChecked",
|
||||
Source: accountKeys[sourceIdx].String(),
|
||||
Mint: accountKeys[mintIdx].String(),
|
||||
Destination: accountKeys[destIdx].String(),
|
||||
Authority: accountKeys[authIdx].String(),
|
||||
Amount: amount,
|
||||
Decimals: &decimals,
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// ParseTransactionTransfers
|
||||
func ParseTransactionTransfers(ctx context.Context, client *rpc.Client, sig string) ([]*TransferInfo, error) {
|
||||
signature, err := solana.SignatureFromBase58(sig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid signature: %w", err)
|
||||
}
|
||||
|
||||
tx, err := client.GetTransaction(
|
||||
ctx,
|
||||
signature,
|
||||
&rpc.GetTransactionOpts{
|
||||
Encoding: solana.EncodingBase64,
|
||||
Commitment: rpc.CommitmentConfirmed,
|
||||
MaxSupportedTransactionVersion: func(v uint64) *uint64 { return &v }(0),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get transaction failed: %w", err)
|
||||
}
|
||||
if tx == nil {
|
||||
return nil, errors.New("transaction not found")
|
||||
}
|
||||
|
||||
decodedTx, err := tx.Transaction.GetTransaction()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode transaction failed: %w", err)
|
||||
}
|
||||
|
||||
msg := decodedTx.Message
|
||||
accountKeys := msg.AccountKeys
|
||||
results := make([]*TransferInfo, 0)
|
||||
|
||||
for _, ix := range msg.Instructions {
|
||||
if int(ix.ProgramIDIndex) >= len(accountKeys) {
|
||||
continue
|
||||
}
|
||||
|
||||
programID := accountKeys[ix.ProgramIDIndex]
|
||||
if !isTokenProgram(programID) {
|
||||
continue
|
||||
}
|
||||
|
||||
data := ix.Data
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch data[0] {
|
||||
case InstructionTransfer:
|
||||
info, err := DecodeTransfer(data, accountKeys, toUint16Slice(ix.Accounts), programID)
|
||||
if err == nil {
|
||||
results = append(results, info)
|
||||
}
|
||||
case InstructionTransferChecked:
|
||||
info, err := DecodeTransferCheck(data, accountKeys, toUint16Slice(ix.Accounts), programID)
|
||||
if err == nil {
|
||||
results = append(results, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func toUint16Slice(in []uint16) []uint16 {
|
||||
out := make([]uint16, len(in))
|
||||
for i, v := range in {
|
||||
out[i] = uint16(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DecodeBase64InstructionData(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
)
|
||||
|
||||
func TestParseSolTransaction(t *testing.T) {
|
||||
client := rpc.New("https://api.mainnet-beta.solana.com")
|
||||
sig := ""
|
||||
|
||||
txInfo, err := ParseTransactionTransfers(context.Background(), client, sig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, item := range txInfo {
|
||||
fmt.Println("type =", item.Type)
|
||||
fmt.Println("program =", item.ProgramID)
|
||||
fmt.Println("from =", item.Source)
|
||||
fmt.Println("to =", item.Destination)
|
||||
fmt.Println("mint =", item.Mint)
|
||||
fmt.Println("authority =", item.Authority)
|
||||
fmt.Println("amount =", item.Amount)
|
||||
if item.Decimals != nil {
|
||||
fmt.Println("decimals =", *item.Decimals)
|
||||
}
|
||||
fmt.Println("-----")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindATAAddress(t *testing.T) {
|
||||
owner := ""
|
||||
mint := "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R" // ray token
|
||||
|
||||
ata, err := FindATAAddress(owner, mint)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("ATA =", ata)
|
||||
}
|
||||
Reference in New Issue
Block a user