new tron gateway
This commit is contained in:
792
api/gateway/tron/client/client.go
Normal file
792
api/gateway/tron/client/client.go
Normal file
@@ -0,0 +1,792 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
chainasset "github.com/tech/sendico/pkg/chain"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
const chainConnectorID = "chain"
|
||||
|
||||
// Client exposes typed helpers around the chain gateway gRPC API.
|
||||
type Client interface {
|
||||
CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
||||
GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
|
||||
ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
||||
GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
||||
SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
|
||||
GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
|
||||
ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
|
||||
EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
|
||||
ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error)
|
||||
EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type grpcConnectorClient interface {
|
||||
GetCapabilities(ctx context.Context, in *connectorv1.GetCapabilitiesRequest, opts ...grpc.CallOption) (*connectorv1.GetCapabilitiesResponse, error)
|
||||
OpenAccount(ctx context.Context, in *connectorv1.OpenAccountRequest, opts ...grpc.CallOption) (*connectorv1.OpenAccountResponse, error)
|
||||
GetAccount(ctx context.Context, in *connectorv1.GetAccountRequest, opts ...grpc.CallOption) (*connectorv1.GetAccountResponse, error)
|
||||
ListAccounts(ctx context.Context, in *connectorv1.ListAccountsRequest, opts ...grpc.CallOption) (*connectorv1.ListAccountsResponse, error)
|
||||
GetBalance(ctx context.Context, in *connectorv1.GetBalanceRequest, opts ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error)
|
||||
SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
|
||||
GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error)
|
||||
ListOperations(ctx context.Context, in *connectorv1.ListOperationsRequest, opts ...grpc.CallOption) (*connectorv1.ListOperationsResponse, error)
|
||||
}
|
||||
|
||||
type chainGatewayClient struct {
|
||||
cfg Config
|
||||
conn *grpc.ClientConn
|
||||
client grpcConnectorClient
|
||||
}
|
||||
|
||||
// New dials the chain gateway endpoint and returns a ready client.
|
||||
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
|
||||
cfg.setDefaults()
|
||||
if strings.TrimSpace(cfg.Address) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: address is required")
|
||||
}
|
||||
|
||||
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
|
||||
defer cancel()
|
||||
|
||||
dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
|
||||
dialOpts = append(dialOpts, opts...)
|
||||
|
||||
if cfg.Insecure {
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
} else {
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
|
||||
if err != nil {
|
||||
return nil, merrors.Internal(fmt.Sprintf("chain-gateway: dial %s: %s", cfg.Address, err.Error()))
|
||||
}
|
||||
|
||||
return &chainGatewayClient{
|
||||
cfg: cfg,
|
||||
conn: conn,
|
||||
client: connectorv1.NewConnectorServiceClient(conn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewWithClient injects a pre-built gateway client (useful for tests).
|
||||
func NewWithClient(cfg Config, gc grpcConnectorClient) Client {
|
||||
cfg.setDefaults()
|
||||
return &chainGatewayClient{
|
||||
cfg: cfg,
|
||||
client: gc,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) Close() error {
|
||||
if c.conn != nil {
|
||||
return c.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
params, err := walletParamsFromRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
label := ""
|
||||
if desc := req.GetDescribable(); desc != nil {
|
||||
label = strings.TrimSpace(desc.GetName())
|
||||
}
|
||||
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
Kind: connectorv1.AccountKind_CHAIN_MANAGED_WALLET,
|
||||
Asset: chainasset.AssetString(req.GetAsset()),
|
||||
OwnerRef: strings.TrimSpace(req.GetOwnerRef()),
|
||||
Label: label,
|
||||
Params: params,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.GetError() != nil {
|
||||
return nil, connectorError(resp.GetError())
|
||||
}
|
||||
wallet := managedWalletFromAccount(resp.GetAccount())
|
||||
return &chainv1.CreateManagedWalletResponse{Wallet: wallet}, nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
|
||||
}
|
||||
resp, err := c.client.GetAccount(ctx, &connectorv1.GetAccountRequest{AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chainv1.GetManagedWalletResponse{Wallet: managedWalletFromAccount(resp.GetAccount())}, nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
assetString := ""
|
||||
var ownerRefFilter *wrapperspb.StringValue
|
||||
orgRef := ""
|
||||
var page *paginationv1.CursorPageRequest
|
||||
if req != nil {
|
||||
assetString = chainasset.AssetString(req.GetAsset())
|
||||
ownerRefFilter = req.GetOwnerRefFilter()
|
||||
orgRef = strings.TrimSpace(req.GetOrganizationRef())
|
||||
page = req.GetPage()
|
||||
}
|
||||
resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{
|
||||
OwnerRefFilter: ownerRefFilter,
|
||||
OrganizationRef: orgRef,
|
||||
Kind: connectorv1.AccountKind_CHAIN_MANAGED_WALLET,
|
||||
Asset: assetString,
|
||||
Page: page,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wallets := make([]*chainv1.ManagedWallet, 0, len(resp.GetAccounts()))
|
||||
for _, account := range resp.GetAccounts() {
|
||||
wallets = append(wallets, managedWalletFromAccount(account))
|
||||
}
|
||||
return &chainv1.ListManagedWalletsResponse{Wallets: wallets, Page: resp.GetPage()}, nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
|
||||
}
|
||||
resp, err := c.client.GetBalance(ctx, &connectorv1.GetBalanceRequest{AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
balance := resp.GetBalance()
|
||||
if balance == nil {
|
||||
return nil, merrors.Internal("chain-gateway: balance response missing")
|
||||
}
|
||||
return &chainv1.GetWalletBalanceResponse{Balance: &chainv1.WalletBalance{
|
||||
Available: balance.GetAvailable(),
|
||||
PendingInbound: balance.GetPendingInbound(),
|
||||
PendingOutbound: balance.GetPendingOutbound(),
|
||||
CalculatedAt: balance.GetCalculatedAt(),
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
||||
}
|
||||
operation, err := operationFromTransfer(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||
return nil, connectorError(resp.GetReceipt().GetError())
|
||||
}
|
||||
transfer := transferFromReceipt(req, resp.GetReceipt())
|
||||
return &chainv1.SubmitTransferResponse{Transfer: transfer}, nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
if req == nil || strings.TrimSpace(req.GetTransferRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: transfer_ref is required")
|
||||
}
|
||||
resp, err := c.client.GetOperation(ctx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(req.GetTransferRef())})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chainv1.GetTransferResponse{Transfer: transferFromOperation(resp.GetOperation())}, nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
source := ""
|
||||
status := chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||
var page *paginationv1.CursorPageRequest
|
||||
if req != nil {
|
||||
source = strings.TrimSpace(req.GetSourceWalletRef())
|
||||
status = req.GetStatus()
|
||||
page = req.GetPage()
|
||||
}
|
||||
resp, err := c.client.ListOperations(ctx, &connectorv1.ListOperationsRequest{
|
||||
AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: source},
|
||||
Status: operationStatusFromTransfer(status),
|
||||
Page: page,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transfers := make([]*chainv1.Transfer, 0, len(resp.GetOperations()))
|
||||
for _, op := range resp.GetOperations() {
|
||||
transfers = append(transfers, transferFromOperation(op))
|
||||
}
|
||||
return &chainv1.ListTransfersResponse{Transfers: transfers, Page: resp.GetPage()}, nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
||||
}
|
||||
operation, err := feeEstimateOperation(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||
return nil, connectorError(resp.GetReceipt().GetError())
|
||||
}
|
||||
return estimateFromReceipt(resp.GetReceipt()), nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
|
||||
}
|
||||
operation, err := gasTopUpComputeOperation(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||
return nil, connectorError(resp.GetReceipt().GetError())
|
||||
}
|
||||
return computeGasTopUpFromReceipt(resp.GetReceipt()), nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
||||
}
|
||||
operation, err := gasTopUpEnsureOperation(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||
return nil, connectorError(resp.GetReceipt().GetError())
|
||||
}
|
||||
return ensureGasTopUpFromReceipt(resp.GetReceipt()), nil
|
||||
}
|
||||
|
||||
func (c *chainGatewayClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
timeout := c.cfg.CallTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
|
||||
func walletParamsFromRequest(req *chainv1.CreateManagedWalletRequest) (*structpb.Struct, error) {
|
||||
if req == nil {
|
||||
return nil, nil
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||
}
|
||||
if asset := req.GetAsset(); asset != nil {
|
||||
params["network"] = chainasset.NetworkName(asset.GetChain())
|
||||
params["token_symbol"] = strings.TrimSpace(asset.GetTokenSymbol())
|
||||
params["contract_address"] = strings.TrimSpace(asset.GetContractAddress())
|
||||
}
|
||||
desc := ""
|
||||
if describable := req.GetDescribable(); describable != nil {
|
||||
desc = strings.TrimSpace(describable.GetDescription())
|
||||
}
|
||||
if desc != "" {
|
||||
params["description"] = desc
|
||||
}
|
||||
if len(req.GetMetadata()) > 0 {
|
||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||
}
|
||||
return structpb.NewStruct(params)
|
||||
}
|
||||
|
||||
func managedWalletFromAccount(account *connectorv1.Account) *chainv1.ManagedWallet {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
details := map[string]interface{}{}
|
||||
if account.GetProviderDetails() != nil {
|
||||
details = account.GetProviderDetails().AsMap()
|
||||
}
|
||||
walletRef := ""
|
||||
if ref := account.GetRef(); ref != nil {
|
||||
walletRef = strings.TrimSpace(ref.GetAccountId())
|
||||
}
|
||||
if v := stringFromDetails(details, "wallet_ref"); v != "" {
|
||||
walletRef = v
|
||||
}
|
||||
organizationRef := stringFromDetails(details, "organization_ref")
|
||||
ownerRef := stringFromDetails(details, "owner_ref")
|
||||
if ownerRef == "" {
|
||||
ownerRef = strings.TrimSpace(account.GetOwnerRef())
|
||||
}
|
||||
asset := &chainv1.Asset{
|
||||
Chain: chainasset.NetworkFromString(stringFromDetails(details, "network")),
|
||||
TokenSymbol: strings.TrimSpace(stringFromDetails(details, "token_symbol")),
|
||||
ContractAddress: strings.TrimSpace(stringFromDetails(details, "contract_address")),
|
||||
}
|
||||
if asset.GetTokenSymbol() == "" {
|
||||
asset.TokenSymbol = strings.TrimSpace(chainasset.TokenFromAssetString(account.GetAsset()))
|
||||
}
|
||||
describable := account.GetDescribable()
|
||||
label := strings.TrimSpace(account.GetLabel())
|
||||
if describable == nil {
|
||||
if label != "" {
|
||||
describable = &describablev1.Describable{Name: label}
|
||||
}
|
||||
} else if strings.TrimSpace(describable.GetName()) == "" && label != "" {
|
||||
desc := strings.TrimSpace(describable.GetDescription())
|
||||
if desc == "" {
|
||||
describable = &describablev1.Describable{Name: label}
|
||||
} else {
|
||||
describable = &describablev1.Describable{Name: label, Description: &desc}
|
||||
}
|
||||
}
|
||||
return &chainv1.ManagedWallet{
|
||||
WalletRef: walletRef,
|
||||
OrganizationRef: organizationRef,
|
||||
OwnerRef: ownerRef,
|
||||
Asset: asset,
|
||||
DepositAddress: stringFromDetails(details, "deposit_address"),
|
||||
Status: managedWalletStatusFromAccount(account.GetState()),
|
||||
CreatedAt: account.GetCreatedAt(),
|
||||
UpdatedAt: account.GetUpdatedAt(),
|
||||
Describable: describable,
|
||||
}
|
||||
}
|
||||
|
||||
func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Operation, error) {
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
||||
}
|
||||
if strings.TrimSpace(req.GetIdempotencyKey()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: idempotency_key is required")
|
||||
}
|
||||
if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
|
||||
}
|
||||
if req.GetDestination() == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: destination is required")
|
||||
}
|
||||
if req.GetAmount() == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: amount is required")
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||
"client_reference": strings.TrimSpace(req.GetClientReference()),
|
||||
}
|
||||
if memo := strings.TrimSpace(req.GetDestination().GetMemo()); memo != "" {
|
||||
params["destination_memo"] = memo
|
||||
}
|
||||
if len(req.GetMetadata()) > 0 {
|
||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||
}
|
||||
if len(req.GetFees()) > 0 {
|
||||
params["fees"] = feesToInterface(req.GetFees())
|
||||
}
|
||||
|
||||
op := &connectorv1.Operation{
|
||||
Type: connectorv1.OperationType_TRANSFER,
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
|
||||
Money: req.GetAmount(),
|
||||
Params: structFromMap(params),
|
||||
}
|
||||
to, err := destinationToParty(req.GetDestination())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
op.To = to
|
||||
setOperationRolesFromMetadata(op, req.GetMetadata())
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func destinationToParty(dest *chainv1.TransferDestination) (*connectorv1.OperationParty, error) {
|
||||
if dest == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: destination is required")
|
||||
}
|
||||
switch d := dest.GetDestination().(type) {
|
||||
case *chainv1.TransferDestination_ManagedWalletRef:
|
||||
return &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(d.ManagedWalletRef)}}}, nil
|
||||
case *chainv1.TransferDestination_ExternalAddress:
|
||||
return &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_External{External: &connectorv1.ExternalRef{ExternalRef: strings.TrimSpace(d.ExternalAddress)}}}, nil
|
||||
default:
|
||||
return nil, merrors.InvalidArgument("chain-gateway: destination is required")
|
||||
}
|
||||
}
|
||||
|
||||
func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[string]string) {
|
||||
if op == nil || len(metadata) == 0 {
|
||||
return
|
||||
}
|
||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" {
|
||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
||||
op.FromRole = pmodel.ToProto(role)
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" {
|
||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
||||
op.ToRole = pmodel.ToProto(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func transferFromReceipt(req *chainv1.SubmitTransferRequest, receipt *connectorv1.OperationReceipt) *chainv1.Transfer {
|
||||
transfer := &chainv1.Transfer{}
|
||||
if req != nil {
|
||||
transfer.IdempotencyKey = strings.TrimSpace(req.GetIdempotencyKey())
|
||||
transfer.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
||||
transfer.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
||||
transfer.Destination = req.GetDestination()
|
||||
transfer.RequestedAmount = req.GetAmount()
|
||||
transfer.NetAmount = req.GetAmount()
|
||||
}
|
||||
if receipt != nil {
|
||||
transfer.TransferRef = strings.TrimSpace(receipt.GetOperationId())
|
||||
transfer.Status = transferStatusFromOperation(receipt.GetStatus())
|
||||
transfer.TransactionHash = strings.TrimSpace(receipt.GetProviderRef())
|
||||
}
|
||||
return transfer
|
||||
}
|
||||
|
||||
func transferFromOperation(op *connectorv1.Operation) *chainv1.Transfer {
|
||||
if op == nil {
|
||||
return nil
|
||||
}
|
||||
transfer := &chainv1.Transfer{
|
||||
TransferRef: strings.TrimSpace(op.GetOperationId()),
|
||||
IdempotencyKey: strings.TrimSpace(op.GetOperationId()),
|
||||
RequestedAmount: op.GetMoney(),
|
||||
NetAmount: op.GetMoney(),
|
||||
Status: transferStatusFromOperation(op.GetStatus()),
|
||||
TransactionHash: strings.TrimSpace(op.GetProviderRef()),
|
||||
CreatedAt: op.GetCreatedAt(),
|
||||
UpdatedAt: op.GetUpdatedAt(),
|
||||
}
|
||||
if from := op.GetFrom(); from != nil && from.GetAccount() != nil {
|
||||
transfer.SourceWalletRef = strings.TrimSpace(from.GetAccount().GetAccountId())
|
||||
}
|
||||
if to := op.GetTo(); to != nil {
|
||||
if account := to.GetAccount(); account != nil {
|
||||
transfer.Destination = &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(account.GetAccountId())}}
|
||||
}
|
||||
if external := to.GetExternal(); external != nil {
|
||||
transfer.Destination = &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(external.GetExternalRef())}}
|
||||
}
|
||||
}
|
||||
return transfer
|
||||
}
|
||||
|
||||
func feeEstimateOperation(req *chainv1.EstimateTransferFeeRequest) (*connectorv1.Operation, error) {
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
||||
}
|
||||
if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
|
||||
}
|
||||
if req.GetDestination() == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: destination is required")
|
||||
}
|
||||
if req.GetAmount() == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: amount is required")
|
||||
}
|
||||
params := map[string]interface{}{}
|
||||
op := &connectorv1.Operation{
|
||||
Type: connectorv1.OperationType_FEE_ESTIMATE,
|
||||
IdempotencyKey: feeEstimateKey(req),
|
||||
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
|
||||
Money: req.GetAmount(),
|
||||
Params: structFromMap(params),
|
||||
}
|
||||
to, err := destinationToParty(req.GetDestination())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
op.To = to
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func estimateFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.EstimateTransferFeeResponse {
|
||||
resp := &chainv1.EstimateTransferFeeResponse{}
|
||||
if receipt == nil || receipt.GetResult() == nil {
|
||||
return resp
|
||||
}
|
||||
data := receipt.GetResult().AsMap()
|
||||
if networkFee, ok := data["network_fee"].(map[string]interface{}); ok {
|
||||
amount := strings.TrimSpace(fmt.Sprint(networkFee["amount"]))
|
||||
currency := strings.TrimSpace(fmt.Sprint(networkFee["currency"]))
|
||||
if amount != "" && currency != "" {
|
||||
resp.NetworkFee = &moneyv1.Money{Amount: amount, Currency: currency}
|
||||
}
|
||||
}
|
||||
if ctx, ok := data["estimation_context"].(string); ok {
|
||||
resp.EstimationContext = strings.TrimSpace(ctx)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func gasTopUpComputeOperation(req *chainv1.ComputeGasTopUpRequest) (*connectorv1.Operation, error) {
|
||||
if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
|
||||
}
|
||||
fee := req.GetEstimatedTotalFee()
|
||||
if fee == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: estimated_total_fee is required")
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"mode": "compute",
|
||||
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
|
||||
}
|
||||
return &connectorv1.Operation{
|
||||
Type: connectorv1.OperationType_GAS_TOPUP,
|
||||
IdempotencyKey: fmt.Sprintf("gas_topup_compute:%s:%s", strings.TrimSpace(req.GetWalletRef()), strings.TrimSpace(fee.GetAmount())),
|
||||
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}}},
|
||||
Params: structFromMap(params),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func gasTopUpEnsureOperation(req *chainv1.EnsureGasTopUpRequest) (*connectorv1.Operation, error) {
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
||||
}
|
||||
if strings.TrimSpace(req.GetIdempotencyKey()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: idempotency_key is required")
|
||||
}
|
||||
if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
|
||||
}
|
||||
if strings.TrimSpace(req.GetTargetWalletRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: target_wallet_ref is required")
|
||||
}
|
||||
fee := req.GetEstimatedTotalFee()
|
||||
if fee == nil {
|
||||
return nil, merrors.InvalidArgument("chain-gateway: estimated_total_fee is required")
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"mode": "ensure",
|
||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||
"target_wallet_ref": strings.TrimSpace(req.GetTargetWalletRef()),
|
||||
"client_reference": strings.TrimSpace(req.GetClientReference()),
|
||||
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
|
||||
}
|
||||
if len(req.GetMetadata()) > 0 {
|
||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||
}
|
||||
return &connectorv1.Operation{
|
||||
Type: connectorv1.OperationType_GAS_TOPUP,
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
|
||||
Params: structFromMap(params),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func computeGasTopUpFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.ComputeGasTopUpResponse {
|
||||
resp := &chainv1.ComputeGasTopUpResponse{}
|
||||
if receipt == nil || receipt.GetResult() == nil {
|
||||
return resp
|
||||
}
|
||||
data := receipt.GetResult().AsMap()
|
||||
if amount, ok := data["topup_amount"].(map[string]interface{}); ok {
|
||||
resp.TopupAmount = &moneyv1.Money{
|
||||
Amount: strings.TrimSpace(fmt.Sprint(amount["amount"])),
|
||||
Currency: strings.TrimSpace(fmt.Sprint(amount["currency"])),
|
||||
}
|
||||
}
|
||||
if capHit, ok := data["cap_hit"].(bool); ok {
|
||||
resp.CapHit = capHit
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func ensureGasTopUpFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.EnsureGasTopUpResponse {
|
||||
resp := &chainv1.EnsureGasTopUpResponse{}
|
||||
if receipt == nil || receipt.GetResult() == nil {
|
||||
return resp
|
||||
}
|
||||
data := receipt.GetResult().AsMap()
|
||||
if amount, ok := data["topup_amount"].(map[string]interface{}); ok {
|
||||
resp.TopupAmount = &moneyv1.Money{
|
||||
Amount: strings.TrimSpace(fmt.Sprint(amount["amount"])),
|
||||
Currency: strings.TrimSpace(fmt.Sprint(amount["currency"])),
|
||||
}
|
||||
}
|
||||
if capHit, ok := data["cap_hit"].(bool); ok {
|
||||
resp.CapHit = capHit
|
||||
}
|
||||
if transferRef, ok := data["transfer_ref"].(string); ok {
|
||||
resp.Transfer = &chainv1.Transfer{TransferRef: strings.TrimSpace(transferRef)}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func feeEstimateKey(req *chainv1.EstimateTransferFeeRequest) string {
|
||||
if req == nil || req.GetAmount() == nil {
|
||||
return "fee_estimate"
|
||||
}
|
||||
return fmt.Sprintf("fee_estimate:%s:%s:%s", strings.TrimSpace(req.GetSourceWalletRef()), strings.TrimSpace(req.GetAmount().GetCurrency()), strings.TrimSpace(req.GetAmount().GetAmount()))
|
||||
}
|
||||
|
||||
func connectorError(err *connectorv1.ConnectorError) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
msg := strings.TrimSpace(err.GetMessage())
|
||||
switch err.GetCode() {
|
||||
case connectorv1.ErrorCode_INVALID_PARAMS:
|
||||
return merrors.InvalidArgument(msg)
|
||||
case connectorv1.ErrorCode_NOT_FOUND:
|
||||
return merrors.NoData(msg)
|
||||
case connectorv1.ErrorCode_UNSUPPORTED_OPERATION, connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND:
|
||||
return merrors.NotImplemented(msg)
|
||||
case connectorv1.ErrorCode_RATE_LIMITED, connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE:
|
||||
return merrors.Internal(msg)
|
||||
default:
|
||||
return merrors.Internal(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func structFromMap(data map[string]interface{}) *structpb.Struct {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
result, err := structpb.NewStruct(data)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mapStringToInterface(input map[string]string) map[string]interface{} {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]interface{}, len(input))
|
||||
for k, v := range input {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func feesToInterface(fees []*chainv1.ServiceFeeBreakdown) []interface{} {
|
||||
if len(fees) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]interface{}, 0, len(fees))
|
||||
for _, fee := range fees {
|
||||
if fee == nil || fee.GetAmount() == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, map[string]interface{}{
|
||||
"fee_code": strings.TrimSpace(fee.GetFeeCode()),
|
||||
"description": strings.TrimSpace(fee.GetDescription()),
|
||||
"amount": strings.TrimSpace(fee.GetAmount().GetAmount()),
|
||||
"currency": strings.TrimSpace(fee.GetAmount().GetCurrency()),
|
||||
})
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stringFromDetails(details map[string]interface{}, key string) string {
|
||||
if details == nil {
|
||||
return ""
|
||||
}
|
||||
if value, ok := details[key]; ok {
|
||||
return strings.TrimSpace(fmt.Sprint(value))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func managedWalletStatusFromAccount(state connectorv1.AccountState) chainv1.ManagedWalletStatus {
|
||||
switch state {
|
||||
case connectorv1.AccountState_ACCOUNT_ACTIVE:
|
||||
return chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
|
||||
case connectorv1.AccountState_ACCOUNT_SUSPENDED:
|
||||
return chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
|
||||
case connectorv1.AccountState_ACCOUNT_CLOSED:
|
||||
return chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
|
||||
default:
|
||||
return chainv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func transferStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
||||
switch status {
|
||||
case connectorv1.OperationStatus_CONFIRMED:
|
||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||
case connectorv1.OperationStatus_FAILED:
|
||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||
case connectorv1.OperationStatus_CANCELED:
|
||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||
default:
|
||||
return chainv1.TransferStatus_TRANSFER_PENDING
|
||||
}
|
||||
}
|
||||
|
||||
func operationStatusFromTransfer(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||
switch status {
|
||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||
return connectorv1.OperationStatus_CONFIRMED
|
||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||
return connectorv1.OperationStatus_FAILED
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return connectorv1.OperationStatus_CANCELED
|
||||
default:
|
||||
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
68
api/gateway/tron/client/client_test.go
Normal file
68
api/gateway/tron/client/client_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
type stubConnectorClient struct {
|
||||
listReq *connectorv1.ListAccountsRequest
|
||||
}
|
||||
|
||||
func (s *stubConnectorClient) GetCapabilities(ctx context.Context, in *connectorv1.GetCapabilitiesRequest, opts ...grpc.CallOption) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||
return &connectorv1.GetCapabilitiesResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *stubConnectorClient) OpenAccount(ctx context.Context, in *connectorv1.OpenAccountRequest, opts ...grpc.CallOption) (*connectorv1.OpenAccountResponse, error) {
|
||||
return &connectorv1.OpenAccountResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *stubConnectorClient) GetAccount(ctx context.Context, in *connectorv1.GetAccountRequest, opts ...grpc.CallOption) (*connectorv1.GetAccountResponse, error) {
|
||||
return &connectorv1.GetAccountResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *stubConnectorClient) ListAccounts(ctx context.Context, in *connectorv1.ListAccountsRequest, opts ...grpc.CallOption) (*connectorv1.ListAccountsResponse, error) {
|
||||
s.listReq = in
|
||||
return &connectorv1.ListAccountsResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *stubConnectorClient) GetBalance(ctx context.Context, in *connectorv1.GetBalanceRequest, opts ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error) {
|
||||
return &connectorv1.GetBalanceResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *stubConnectorClient) SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error) {
|
||||
return &connectorv1.SubmitOperationResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *stubConnectorClient) GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error) {
|
||||
return &connectorv1.GetOperationResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *stubConnectorClient) ListOperations(ctx context.Context, in *connectorv1.ListOperationsRequest, opts ...grpc.CallOption) (*connectorv1.ListOperationsResponse, error) {
|
||||
return &connectorv1.ListOperationsResponse{}, nil
|
||||
}
|
||||
|
||||
func TestListManagedWallets_ForwardsOrganizationRef(t *testing.T) {
|
||||
stub := &stubConnectorClient{}
|
||||
client := NewWithClient(Config{}, stub)
|
||||
|
||||
_, err := client.ListManagedWallets(context.Background(), &chainv1.ListManagedWalletsRequest{
|
||||
OrganizationRef: "org-1",
|
||||
OwnerRefFilter: wrapperspb.String("owner-1"),
|
||||
Asset: &chainv1.Asset{
|
||||
Chain: chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
||||
TokenSymbol: "USDC",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stub.listReq)
|
||||
require.Equal(t, "org-1", stub.listReq.GetOrganizationRef())
|
||||
require.Equal(t, "owner-1", stub.listReq.GetOwnerRefFilter().GetValue())
|
||||
require.Equal(t, connectorv1.AccountKind_CHAIN_MANAGED_WALLET, stub.listReq.GetKind())
|
||||
}
|
||||
20
api/gateway/tron/client/config.go
Normal file
20
api/gateway/tron/client/config.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package client
|
||||
|
||||
import "time"
|
||||
|
||||
// Config captures connection settings for the chain gateway gRPC service.
|
||||
type Config struct {
|
||||
Address string
|
||||
DialTimeout time.Duration
|
||||
CallTimeout time.Duration
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
func (c *Config) setDefaults() {
|
||||
if c.DialTimeout <= 0 {
|
||||
c.DialTimeout = 5 * time.Second
|
||||
}
|
||||
if c.CallTimeout <= 0 {
|
||||
c.CallTimeout = 3 * time.Second
|
||||
}
|
||||
}
|
||||
99
api/gateway/tron/client/fake.go
Normal file
99
api/gateway/tron/client/fake.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
)
|
||||
|
||||
// Fake implements Client for tests.
|
||||
type Fake struct {
|
||||
CreateManagedWalletFn func(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
||||
GetManagedWalletFn func(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
|
||||
ListManagedWalletsFn func(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
||||
GetWalletBalanceFn func(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
||||
SubmitTransferFn func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
|
||||
GetTransferFn func(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
|
||||
ListTransfersFn func(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
|
||||
EstimateTransferFeeFn func(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
|
||||
ComputeGasTopUpFn func(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error)
|
||||
EnsureGasTopUpFn func(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error)
|
||||
CloseFn func() error
|
||||
}
|
||||
|
||||
func (f *Fake) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||
if f.CreateManagedWalletFn != nil {
|
||||
return f.CreateManagedWalletFn(ctx, req)
|
||||
}
|
||||
return &chainv1.CreateManagedWalletResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||
if f.GetManagedWalletFn != nil {
|
||||
return f.GetManagedWalletFn(ctx, req)
|
||||
}
|
||||
return &chainv1.GetManagedWalletResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||
if f.ListManagedWalletsFn != nil {
|
||||
return f.ListManagedWalletsFn(ctx, req)
|
||||
}
|
||||
return &chainv1.ListManagedWalletsResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||
if f.GetWalletBalanceFn != nil {
|
||||
return f.GetWalletBalanceFn(ctx, req)
|
||||
}
|
||||
return &chainv1.GetWalletBalanceResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
if f.SubmitTransferFn != nil {
|
||||
return f.SubmitTransferFn(ctx, req)
|
||||
}
|
||||
return &chainv1.SubmitTransferResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||
if f.GetTransferFn != nil {
|
||||
return f.GetTransferFn(ctx, req)
|
||||
}
|
||||
return &chainv1.GetTransferResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||
if f.ListTransfersFn != nil {
|
||||
return f.ListTransfersFn(ctx, req)
|
||||
}
|
||||
return &chainv1.ListTransfersResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||
if f.EstimateTransferFeeFn != nil {
|
||||
return f.EstimateTransferFeeFn(ctx, req)
|
||||
}
|
||||
return &chainv1.EstimateTransferFeeResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error) {
|
||||
if f.ComputeGasTopUpFn != nil {
|
||||
return f.ComputeGasTopUpFn(ctx, req)
|
||||
}
|
||||
return &chainv1.ComputeGasTopUpResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error) {
|
||||
if f.EnsureGasTopUpFn != nil {
|
||||
return f.EnsureGasTopUpFn(ctx, req)
|
||||
}
|
||||
return &chainv1.EnsureGasTopUpResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) Close() error {
|
||||
if f.CloseFn != nil {
|
||||
return f.CloseFn()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
287
api/gateway/tron/client/rail_gateway.go
Normal file
287
api/gateway/tron/client/rail_gateway.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/payments/rail"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// RailGatewayConfig defines metadata for the rail gateway adapter.
|
||||
type RailGatewayConfig struct {
|
||||
Rail string
|
||||
Network string
|
||||
Capabilities rail.RailCapabilities
|
||||
}
|
||||
|
||||
type chainRailGateway struct {
|
||||
client Client
|
||||
rail string
|
||||
network string
|
||||
capabilities rail.RailCapabilities
|
||||
}
|
||||
|
||||
// NewRailGateway wraps a chain gateway client into a rail gateway adapter.
|
||||
func NewRailGateway(client Client, cfg RailGatewayConfig) rail.RailGateway {
|
||||
railName := strings.ToUpper(strings.TrimSpace(cfg.Rail))
|
||||
if railName == "" {
|
||||
railName = "CRYPTO"
|
||||
}
|
||||
return &chainRailGateway{
|
||||
client: client,
|
||||
rail: railName,
|
||||
network: strings.ToUpper(strings.TrimSpace(cfg.Network)),
|
||||
capabilities: cfg.Capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) Rail() string {
|
||||
return g.rail
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) Network() string {
|
||||
return g.network
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) Capabilities() rail.RailCapabilities {
|
||||
return g.capabilities
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) Send(ctx context.Context, req rail.TransferRequest) (rail.RailResult, error) {
|
||||
if g.client == nil {
|
||||
return rail.RailResult{}, merrors.Internal("chain gateway: client is required")
|
||||
}
|
||||
orgRef := strings.TrimSpace(req.OrganizationRef)
|
||||
if orgRef == "" {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("chain gateway: organization_ref is required")
|
||||
}
|
||||
source := strings.TrimSpace(req.FromAccountID)
|
||||
if source == "" {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("chain gateway: from_account_id is required")
|
||||
}
|
||||
destRef := strings.TrimSpace(req.ToAccountID)
|
||||
if destRef == "" {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("chain gateway: to_account_id is required")
|
||||
}
|
||||
currency := strings.TrimSpace(req.Currency)
|
||||
amountValue := strings.TrimSpace(req.Amount)
|
||||
if currency == "" || amountValue == "" {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("chain gateway: amount is required")
|
||||
}
|
||||
reqNetwork := strings.TrimSpace(req.Network)
|
||||
if g.network != "" && reqNetwork != "" && !strings.EqualFold(g.network, reqNetwork) {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("chain gateway: network mismatch")
|
||||
}
|
||||
if strings.TrimSpace(req.IdempotencyKey) == "" {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("chain gateway: idempotency_key is required")
|
||||
}
|
||||
|
||||
dest, err := g.resolveDestination(ctx, destRef, strings.TrimSpace(req.DestinationMemo))
|
||||
if err != nil {
|
||||
return rail.RailResult{}, err
|
||||
}
|
||||
|
||||
fees := toServiceFees(req.Fees)
|
||||
if len(fees) == 0 && req.Fee != nil {
|
||||
if amt := moneyFromRail(req.Fee); amt != nil {
|
||||
fees = []*chainv1.ServiceFeeBreakdown{{
|
||||
FeeCode: "fee",
|
||||
Amount: amt,
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := g.client.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
|
||||
IdempotencyKey: strings.TrimSpace(req.IdempotencyKey),
|
||||
OrganizationRef: orgRef,
|
||||
SourceWalletRef: source,
|
||||
Destination: dest,
|
||||
Amount: &moneyv1.Money{
|
||||
Currency: currency,
|
||||
Amount: amountValue,
|
||||
},
|
||||
Fees: fees,
|
||||
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
||||
ClientReference: strings.TrimSpace(req.ClientReference),
|
||||
})
|
||||
if err != nil {
|
||||
return rail.RailResult{}, err
|
||||
}
|
||||
if resp == nil || resp.GetTransfer() == nil {
|
||||
return rail.RailResult{}, merrors.Internal("chain gateway: missing transfer response")
|
||||
}
|
||||
|
||||
transfer := resp.GetTransfer()
|
||||
return rail.RailResult{
|
||||
ReferenceID: strings.TrimSpace(transfer.GetTransferRef()),
|
||||
Status: statusFromTransfer(transfer.GetStatus()),
|
||||
FinalAmount: railMoneyFromProto(transfer.GetNetAmount()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) Observe(ctx context.Context, referenceID string) (rail.ObserveResult, error) {
|
||||
if g.client == nil {
|
||||
return rail.ObserveResult{}, merrors.Internal("chain gateway: client is required")
|
||||
}
|
||||
ref := strings.TrimSpace(referenceID)
|
||||
if ref == "" {
|
||||
return rail.ObserveResult{}, merrors.InvalidArgument("chain gateway: reference_id is required")
|
||||
}
|
||||
resp, err := g.client.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: ref})
|
||||
if err != nil {
|
||||
return rail.ObserveResult{}, err
|
||||
}
|
||||
if resp == nil || resp.GetTransfer() == nil {
|
||||
return rail.ObserveResult{}, merrors.Internal("chain gateway: missing transfer response")
|
||||
}
|
||||
transfer := resp.GetTransfer()
|
||||
return rail.ObserveResult{
|
||||
ReferenceID: ref,
|
||||
Status: statusFromTransfer(transfer.GetStatus()),
|
||||
FinalAmount: railMoneyFromProto(transfer.GetNetAmount()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) Block(ctx context.Context, req rail.BlockRequest) (rail.RailResult, error) {
|
||||
return rail.RailResult{}, merrors.NotImplemented("chain gateway: block not supported")
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) Release(ctx context.Context, req rail.ReleaseRequest) (rail.RailResult, error) {
|
||||
return rail.RailResult{}, merrors.NotImplemented("chain gateway: release not supported")
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) resolveDestination(ctx context.Context, destRef, memo string) (*chainv1.TransferDestination, error) {
|
||||
managed, err := g.isManagedWallet(ctx, destRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if managed {
|
||||
return &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: destRef},
|
||||
}, nil
|
||||
}
|
||||
return &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: destRef},
|
||||
Memo: memo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *chainRailGateway) isManagedWallet(ctx context.Context, walletRef string) (bool, error) {
|
||||
resp, err := g.client.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: walletRef})
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.NotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if resp == nil || resp.GetWallet() == nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func statusFromTransfer(status chainv1.TransferStatus) string {
|
||||
switch status {
|
||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||
return rail.TransferStatusSuccess
|
||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||
return rail.TransferStatusFailed
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return rail.TransferStatusRejected
|
||||
case chainv1.TransferStatus_TRANSFER_SIGNING,
|
||||
chainv1.TransferStatus_TRANSFER_PENDING,
|
||||
chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||
return rail.TransferStatusPending
|
||||
default:
|
||||
return rail.TransferStatusPending
|
||||
}
|
||||
}
|
||||
|
||||
func toServiceFees(fees []rail.FeeBreakdown) []*chainv1.ServiceFeeBreakdown {
|
||||
if len(fees) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]*chainv1.ServiceFeeBreakdown, 0, len(fees))
|
||||
for _, fee := range fees {
|
||||
amount := moneyFromRail(fee.Amount)
|
||||
if amount == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, &chainv1.ServiceFeeBreakdown{
|
||||
FeeCode: strings.TrimSpace(fee.FeeCode),
|
||||
Amount: amount,
|
||||
Description: strings.TrimSpace(fee.Description),
|
||||
})
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func moneyFromRail(m *rail.Money) *moneyv1.Money {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
currency := strings.TrimSpace(m.GetCurrency())
|
||||
amount := strings.TrimSpace(m.GetAmount())
|
||||
if currency == "" || amount == "" {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{
|
||||
Currency: currency,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
func railMoneyFromProto(m *moneyv1.Money) *rail.Money {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
currency := strings.TrimSpace(m.GetCurrency())
|
||||
amount := strings.TrimSpace(m.GetAmount())
|
||||
if currency == "" || amount == "" {
|
||||
return nil
|
||||
}
|
||||
return &rail.Money{
|
||||
Currency: currency,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole pmodel.AccountRole) map[string]string {
|
||||
result := cloneMetadata(metadata)
|
||||
if strings.TrimSpace(string(fromRole)) != "" {
|
||||
if result == nil {
|
||||
result = map[string]string{}
|
||||
}
|
||||
result[pmodel.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole))
|
||||
}
|
||||
if strings.TrimSpace(string(toRole)) != "" {
|
||||
if result == nil {
|
||||
result = map[string]string{}
|
||||
}
|
||||
result[pmodel.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func cloneMetadata(input map[string]string) map[string]string {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]string, len(input))
|
||||
for key, value := range input {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user