interface refactoring
This commit is contained in:
@@ -8,13 +8,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
|
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"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const chainConnectorID = "chain"
|
||||||
|
|
||||||
// Client exposes typed helpers around the chain gateway gRPC API.
|
// Client exposes typed helpers around the chain gateway gRPC API.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
||||||
@@ -30,23 +36,21 @@ type Client interface {
|
|||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type grpcGatewayClient interface {
|
type grpcConnectorClient interface {
|
||||||
CreateManagedWallet(ctx context.Context, in *chainv1.CreateManagedWalletRequest, opts ...grpc.CallOption) (*chainv1.CreateManagedWalletResponse, error)
|
GetCapabilities(ctx context.Context, in *connectorv1.GetCapabilitiesRequest, opts ...grpc.CallOption) (*connectorv1.GetCapabilitiesResponse, error)
|
||||||
GetManagedWallet(ctx context.Context, in *chainv1.GetManagedWalletRequest, opts ...grpc.CallOption) (*chainv1.GetManagedWalletResponse, error)
|
OpenAccount(ctx context.Context, in *connectorv1.OpenAccountRequest, opts ...grpc.CallOption) (*connectorv1.OpenAccountResponse, error)
|
||||||
ListManagedWallets(ctx context.Context, in *chainv1.ListManagedWalletsRequest, opts ...grpc.CallOption) (*chainv1.ListManagedWalletsResponse, error)
|
GetAccount(ctx context.Context, in *connectorv1.GetAccountRequest, opts ...grpc.CallOption) (*connectorv1.GetAccountResponse, error)
|
||||||
GetWalletBalance(ctx context.Context, in *chainv1.GetWalletBalanceRequest, opts ...grpc.CallOption) (*chainv1.GetWalletBalanceResponse, error)
|
ListAccounts(ctx context.Context, in *connectorv1.ListAccountsRequest, opts ...grpc.CallOption) (*connectorv1.ListAccountsResponse, error)
|
||||||
SubmitTransfer(ctx context.Context, in *chainv1.SubmitTransferRequest, opts ...grpc.CallOption) (*chainv1.SubmitTransferResponse, error)
|
GetBalance(ctx context.Context, in *connectorv1.GetBalanceRequest, opts ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error)
|
||||||
GetTransfer(ctx context.Context, in *chainv1.GetTransferRequest, opts ...grpc.CallOption) (*chainv1.GetTransferResponse, error)
|
SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
|
||||||
ListTransfers(ctx context.Context, in *chainv1.ListTransfersRequest, opts ...grpc.CallOption) (*chainv1.ListTransfersResponse, error)
|
GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error)
|
||||||
EstimateTransferFee(ctx context.Context, in *chainv1.EstimateTransferFeeRequest, opts ...grpc.CallOption) (*chainv1.EstimateTransferFeeResponse, error)
|
ListOperations(ctx context.Context, in *connectorv1.ListOperationsRequest, opts ...grpc.CallOption) (*connectorv1.ListOperationsResponse, error)
|
||||||
ComputeGasTopUp(ctx context.Context, in *chainv1.ComputeGasTopUpRequest, opts ...grpc.CallOption) (*chainv1.ComputeGasTopUpResponse, error)
|
|
||||||
EnsureGasTopUp(ctx context.Context, in *chainv1.EnsureGasTopUpRequest, opts ...grpc.CallOption) (*chainv1.EnsureGasTopUpResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type chainGatewayClient struct {
|
type chainGatewayClient struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
conn *grpc.ClientConn
|
conn *grpc.ClientConn
|
||||||
client grpcGatewayClient
|
client grpcConnectorClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// New dials the chain gateway endpoint and returns a ready client.
|
// New dials the chain gateway endpoint and returns a ready client.
|
||||||
@@ -76,12 +80,12 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
|||||||
return &chainGatewayClient{
|
return &chainGatewayClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
client: unifiedv1.NewUnifiedGatewayServiceClient(conn),
|
client: connectorv1.NewConnectorServiceClient(conn),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWithClient injects a pre-built gateway client (useful for tests).
|
// NewWithClient injects a pre-built gateway client (useful for tests).
|
||||||
func NewWithClient(cfg Config, gc grpcGatewayClient) Client {
|
func NewWithClient(cfg Config, gc grpcConnectorClient) Client {
|
||||||
cfg.setDefaults()
|
cfg.setDefaults()
|
||||||
return &chainGatewayClient{
|
return &chainGatewayClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -99,61 +103,213 @@ func (c *chainGatewayClient) Close() error {
|
|||||||
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.CreateManagedWallet(ctx, req)
|
|
||||||
|
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: assetStringFromChainAsset(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) {
|
func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetManagedWallet(ctx, req)
|
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) {
|
func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ListManagedWallets(ctx, req)
|
assetString := ""
|
||||||
|
ownerRef := ""
|
||||||
|
var page *paginationv1.CursorPageRequest
|
||||||
|
if req != nil {
|
||||||
|
assetString = assetStringFromChainAsset(req.GetAsset())
|
||||||
|
ownerRef = strings.TrimSpace(req.GetOwnerRef())
|
||||||
|
page = req.GetPage()
|
||||||
|
}
|
||||||
|
resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{
|
||||||
|
OwnerRef: ownerRef,
|
||||||
|
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) {
|
func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetWalletBalance(ctx, req)
|
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) {
|
func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.SubmitTransfer(ctx, req)
|
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) {
|
func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetTransfer(ctx, req)
|
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) {
|
func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ListTransfers(ctx, req)
|
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) {
|
func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.EstimateTransferFee(ctx, req)
|
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) {
|
func (c *chainGatewayClient) ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ComputeGasTopUp(ctx, req)
|
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) {
|
func (c *chainGatewayClient) EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.EnsureGasTopUp(ctx, req)
|
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) {
|
func (c *chainGatewayClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
@@ -163,3 +319,495 @@ func (c *chainGatewayClient) callContext(ctx context.Context) (context.Context,
|
|||||||
}
|
}
|
||||||
return context.WithTimeout(ctx, timeout)
|
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"] = asset.GetChain().String()
|
||||||
|
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: chainNetworkFromString(stringFromDetails(details, "network")),
|
||||||
|
TokenSymbol: strings.TrimSpace(stringFromDetails(details, "token_symbol")),
|
||||||
|
ContractAddress: strings.TrimSpace(stringFromDetails(details, "contract_address")),
|
||||||
|
}
|
||||||
|
if asset.GetTokenSymbol() == "" {
|
||||||
|
asset.TokenSymbol = strings.TrimSpace(tokenFromAssetString(account.GetAsset()))
|
||||||
|
}
|
||||||
|
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: &describablev1.Describable{
|
||||||
|
Name: strings.TrimSpace(account.GetLabel()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assetStringFromChainAsset(asset *chainv1.Asset) string {
|
||||||
|
if asset == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
symbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
||||||
|
if symbol == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
suffix := chainAssetSuffix(asset.GetChain())
|
||||||
|
if suffix == "" {
|
||||||
|
return symbol
|
||||||
|
}
|
||||||
|
return symbol + "-" + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainAssetSuffix(chain chainv1.ChainNetwork) string {
|
||||||
|
switch chain {
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET:
|
||||||
|
return "ETH"
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE:
|
||||||
|
return "ARB"
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET:
|
||||||
|
return "TRC20"
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE:
|
||||||
|
return "TRC20"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenFromAssetString(asset string) string {
|
||||||
|
if asset == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if idx := strings.Index(asset, "-"); idx > 0 {
|
||||||
|
return asset[:idx]
|
||||||
|
}
|
||||||
|
return asset
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainNetworkFromString(value string) chainv1.ChainNetwork {
|
||||||
|
value = strings.ToUpper(strings.TrimSpace(value))
|
||||||
|
if value == "" {
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
|
if val, ok := chainv1.ChainNetwork_value[value]; ok {
|
||||||
|
return chainv1.ChainNetwork(val)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(value, "CHAIN_NETWORK_") {
|
||||||
|
value = "CHAIN_NETWORK_" + value
|
||||||
|
}
|
||||||
|
if val, ok := chainv1.ChainNetwork_value[value]; ok {
|
||||||
|
return chainv1.ChainNetwork(val)
|
||||||
|
}
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|||||||
691
api/gateway/chain/internal/service/gateway/connector.go
Normal file
691
api/gateway/chain/internal/service/gateway/connector.go
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/appversion"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const chainConnectorID = "chain"
|
||||||
|
|
||||||
|
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||||
|
return &connectorv1.GetCapabilitiesResponse{
|
||||||
|
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||||
|
ConnectorType: chainConnectorID,
|
||||||
|
Version: appversion.Create().Short(),
|
||||||
|
SupportedAccountKinds: []connectorv1.AccountKind{connectorv1.AccountKind_CHAIN_MANAGED_WALLET},
|
||||||
|
SupportedOperationTypes: []connectorv1.OperationType{
|
||||||
|
connectorv1.OperationType_TRANSFER,
|
||||||
|
connectorv1.OperationType_FEE_ESTIMATE,
|
||||||
|
connectorv1.OperationType_GAS_TOPUP,
|
||||||
|
},
|
||||||
|
OpenAccountParams: chainOpenAccountParams(),
|
||||||
|
OperationParams: chainOperationParams(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
|
||||||
|
if req == nil {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil, "")}, nil
|
||||||
|
}
|
||||||
|
if req.GetKind() != connectorv1.AccountKind_CHAIN_MANAGED_WALLET {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil, "")}, nil
|
||||||
|
}
|
||||||
|
reader := params.New(req.GetParams())
|
||||||
|
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||||||
|
if orgRef == "" {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil, "")}, nil
|
||||||
|
}
|
||||||
|
asset, err := parseChainAsset(strings.TrimSpace(req.GetAsset()), reader)
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil, "")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.CreateManagedWallet(ctx, &chainv1.CreateManagedWalletRequest{
|
||||||
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||||
|
OrganizationRef: orgRef,
|
||||||
|
OwnerRef: strings.TrimSpace(req.GetOwnerRef()),
|
||||||
|
Asset: asset,
|
||||||
|
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||||
|
Describable: describableFromLabel(req.GetLabel(), reader.String("description")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil
|
||||||
|
}
|
||||||
|
return &connectorv1.OpenAccountResponse{Account: chainWalletToAccount(resp.GetWallet())}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAccount(ctx context.Context, req *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
|
||||||
|
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("get_account: account_ref.account_id is required")
|
||||||
|
}
|
||||||
|
resp, err := s.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: strings.TrimSpace(req.GetAccountRef().GetAccountId())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &connectorv1.GetAccountResponse{Account: chainWalletToAccount(resp.GetWallet())}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListAccounts(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, merrors.InvalidArgument("list_accounts: request is required")
|
||||||
|
}
|
||||||
|
asset := (*chainv1.Asset)(nil)
|
||||||
|
if assetString := strings.TrimSpace(req.GetAsset()); assetString != "" {
|
||||||
|
parsed, err := parseChainAsset(assetString, params.New(nil))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.InvalidArgument(err.Error())
|
||||||
|
}
|
||||||
|
asset = parsed
|
||||||
|
}
|
||||||
|
resp, err := s.ListManagedWallets(ctx, &chainv1.ListManagedWalletsRequest{
|
||||||
|
OwnerRef: strings.TrimSpace(req.GetOwnerRef()),
|
||||||
|
Asset: asset,
|
||||||
|
Page: req.GetPage(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
accounts := make([]*connectorv1.Account, 0, len(resp.GetWallets()))
|
||||||
|
for _, wallet := range resp.GetWallets() {
|
||||||
|
accounts = append(accounts, chainWalletToAccount(wallet))
|
||||||
|
}
|
||||||
|
return &connectorv1.ListAccountsResponse{
|
||||||
|
Accounts: accounts,
|
||||||
|
Page: resp.GetPage(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetBalance(ctx context.Context, req *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
|
||||||
|
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("get_balance: account_ref.account_id is required")
|
||||||
|
}
|
||||||
|
resp, err := s.GetWalletBalance(ctx, &chainv1.GetWalletBalanceRequest{WalletRef: strings.TrimSpace(req.GetAccountRef().GetAccountId())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bal := resp.GetBalance()
|
||||||
|
return &connectorv1.GetBalanceResponse{
|
||||||
|
Balance: &connectorv1.Balance{
|
||||||
|
AccountRef: req.GetAccountRef(),
|
||||||
|
Available: bal.GetAvailable(),
|
||||||
|
PendingInbound: bal.GetPendingInbound(),
|
||||||
|
PendingOutbound: bal.GetPendingOutbound(),
|
||||||
|
CalculatedAt: bal.GetCalculatedAt(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
||||||
|
if req == nil || req.GetOperation() == nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil
|
||||||
|
}
|
||||||
|
op := req.GetOperation()
|
||||||
|
if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
reader := params.New(op.GetParams())
|
||||||
|
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||||||
|
source := operationAccountID(op.GetFrom())
|
||||||
|
if source == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "operation: from.account is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch op.GetType() {
|
||||||
|
case connectorv1.OperationType_TRANSFER:
|
||||||
|
dest, err := transferDestinationFromOperation(op)
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
amount := op.GetMoney()
|
||||||
|
if amount == nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
amount = normalizeMoneyForChain(amount)
|
||||||
|
if orgRef == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: organization_ref is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
resp, err := s.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
|
||||||
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
OrganizationRef: orgRef,
|
||||||
|
SourceWalletRef: source,
|
||||||
|
Destination: dest,
|
||||||
|
Amount: amount,
|
||||||
|
Fees: parseChainFees(reader),
|
||||||
|
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||||
|
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
transfer := resp.GetTransfer()
|
||||||
|
return &connectorv1.SubmitOperationResponse{
|
||||||
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
|
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
|
||||||
|
Status: chainTransferStatusToOperation(transfer.GetStatus()),
|
||||||
|
ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case connectorv1.OperationType_FEE_ESTIMATE:
|
||||||
|
dest, err := transferDestinationFromOperation(op)
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
amount := op.GetMoney()
|
||||||
|
if amount == nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "estimate: money is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
amount = normalizeMoneyForChain(amount)
|
||||||
|
opID := strings.TrimSpace(op.GetOperationId())
|
||||||
|
if opID == "" {
|
||||||
|
opID = strings.TrimSpace(op.GetIdempotencyKey())
|
||||||
|
}
|
||||||
|
resp, err := s.EstimateTransferFee(ctx, &chainv1.EstimateTransferFeeRequest{
|
||||||
|
SourceWalletRef: source,
|
||||||
|
Destination: dest,
|
||||||
|
Amount: amount,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
result := feeEstimateResult(resp)
|
||||||
|
return &connectorv1.SubmitOperationResponse{
|
||||||
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
|
OperationId: opID,
|
||||||
|
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||||
|
Result: result,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case connectorv1.OperationType_GAS_TOPUP:
|
||||||
|
fee, err := parseMoneyFromMap(reader.Map("estimated_total_fee"))
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
fee = normalizeMoneyForChain(fee)
|
||||||
|
mode := strings.ToLower(strings.TrimSpace(reader.String("mode")))
|
||||||
|
if mode == "" {
|
||||||
|
mode = "compute"
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case "compute":
|
||||||
|
opID := strings.TrimSpace(op.GetOperationId())
|
||||||
|
if opID == "" {
|
||||||
|
opID = strings.TrimSpace(op.GetIdempotencyKey())
|
||||||
|
}
|
||||||
|
resp, err := s.ComputeGasTopUp(ctx, &chainv1.ComputeGasTopUpRequest{
|
||||||
|
WalletRef: source,
|
||||||
|
EstimatedTotalFee: fee,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
return &connectorv1.SubmitOperationResponse{
|
||||||
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
|
OperationId: opID,
|
||||||
|
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||||
|
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case "ensure":
|
||||||
|
opID := strings.TrimSpace(op.GetOperationId())
|
||||||
|
if opID == "" {
|
||||||
|
opID = strings.TrimSpace(op.GetIdempotencyKey())
|
||||||
|
}
|
||||||
|
if orgRef == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: organization_ref is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
target := strings.TrimSpace(reader.String("target_wallet_ref"))
|
||||||
|
if target == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: target_wallet_ref is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
||||||
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
OrganizationRef: orgRef,
|
||||||
|
SourceWalletRef: source,
|
||||||
|
TargetWalletRef: target,
|
||||||
|
EstimatedTotalFee: fee,
|
||||||
|
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||||
|
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
transferRef := ""
|
||||||
|
if transfer := resp.GetTransfer(); transfer != nil {
|
||||||
|
transferRef = strings.TrimSpace(transfer.GetTransferRef())
|
||||||
|
}
|
||||||
|
return &connectorv1.SubmitOperationResponse{
|
||||||
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
|
OperationId: opID,
|
||||||
|
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||||
|
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: invalid mode", op, "")}}, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
|
||||||
|
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
|
||||||
|
}
|
||||||
|
resp, err := s.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: strings.TrimSpace(req.GetOperationId())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &connectorv1.GetOperationResponse{Operation: chainTransferToOperation(resp.GetTransfer())}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListOperations(ctx context.Context, req *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, merrors.InvalidArgument("list_operations: request is required")
|
||||||
|
}
|
||||||
|
source := ""
|
||||||
|
if req.GetAccountRef() != nil {
|
||||||
|
source = strings.TrimSpace(req.GetAccountRef().GetAccountId())
|
||||||
|
}
|
||||||
|
resp, err := s.ListTransfers(ctx, &chainv1.ListTransfersRequest{
|
||||||
|
SourceWalletRef: source,
|
||||||
|
Status: chainStatusFromOperation(req.GetStatus()),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ops := make([]*connectorv1.Operation, 0, len(resp.GetTransfers()))
|
||||||
|
for _, transfer := range resp.GetTransfers() {
|
||||||
|
ops = append(ops, chainTransferToOperation(transfer))
|
||||||
|
}
|
||||||
|
return &connectorv1.ListOperationsResponse{Operations: ops, Page: resp.GetPage()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainOpenAccountParams() []*connectorv1.ParamSpec {
|
||||||
|
return []*connectorv1.ParamSpec{
|
||||||
|
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference for the wallet."},
|
||||||
|
{Key: "network", Type: connectorv1.ParamType_STRING, Required: true, Description: "Blockchain network name."},
|
||||||
|
{Key: "token_symbol", Type: connectorv1.ParamType_STRING, Required: true, Description: "Token symbol (e.g., USDT)."},
|
||||||
|
{Key: "contract_address", Type: connectorv1.ParamType_STRING, Required: false, Description: "Token contract address override."},
|
||||||
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Additional metadata map."},
|
||||||
|
{Key: "description", Type: connectorv1.ParamType_STRING, Required: false, Description: "Wallet description."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainOperationParams() []*connectorv1.OperationParamSpec {
|
||||||
|
return []*connectorv1.OperationParamSpec{
|
||||||
|
{OperationType: connectorv1.OperationType_TRANSFER, Params: []*connectorv1.ParamSpec{
|
||||||
|
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference."},
|
||||||
|
{Key: "destination_memo", Type: connectorv1.ParamType_STRING, Required: false, Description: "Destination memo/tag."},
|
||||||
|
{Key: "client_reference", Type: connectorv1.ParamType_STRING, Required: false, Description: "Client reference id."},
|
||||||
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Transfer metadata."},
|
||||||
|
{Key: "fees", Type: connectorv1.ParamType_JSON, Required: false, Description: "Service fee breakdowns."},
|
||||||
|
}},
|
||||||
|
{OperationType: connectorv1.OperationType_FEE_ESTIMATE, Params: []*connectorv1.ParamSpec{
|
||||||
|
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Organization reference."},
|
||||||
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Estimate metadata."},
|
||||||
|
}},
|
||||||
|
{OperationType: connectorv1.OperationType_GAS_TOPUP, Params: []*connectorv1.ParamSpec{
|
||||||
|
{Key: "mode", Type: connectorv1.ParamType_STRING, Required: false, Description: "compute | ensure."},
|
||||||
|
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Organization reference (required for ensure)."},
|
||||||
|
{Key: "target_wallet_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Target wallet ref (ensure)."},
|
||||||
|
{Key: "estimated_total_fee", Type: connectorv1.ParamType_JSON, Required: true, Description: "Estimated total fee {amount,currency}."},
|
||||||
|
{Key: "client_reference", Type: connectorv1.ParamType_STRING, Required: false, Description: "Client reference."},
|
||||||
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Top-up metadata."},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainWalletToAccount(wallet *chainv1.ManagedWallet) *connectorv1.Account {
|
||||||
|
if wallet == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
details, _ := structpb.NewStruct(map[string]interface{}{
|
||||||
|
"deposit_address": wallet.GetDepositAddress(),
|
||||||
|
"organization_ref": wallet.GetOrganizationRef(),
|
||||||
|
"owner_ref": wallet.GetOwnerRef(),
|
||||||
|
"network": wallet.GetAsset().GetChain().String(),
|
||||||
|
"token_symbol": wallet.GetAsset().GetTokenSymbol(),
|
||||||
|
"contract_address": wallet.GetAsset().GetContractAddress(),
|
||||||
|
"wallet_ref": wallet.GetWalletRef(),
|
||||||
|
})
|
||||||
|
return &connectorv1.Account{
|
||||||
|
Ref: &connectorv1.AccountRef{
|
||||||
|
ConnectorId: chainConnectorID,
|
||||||
|
AccountId: strings.TrimSpace(wallet.GetWalletRef()),
|
||||||
|
},
|
||||||
|
Kind: connectorv1.AccountKind_CHAIN_MANAGED_WALLET,
|
||||||
|
Asset: assetStringFromChainAsset(wallet.GetAsset()),
|
||||||
|
State: chainWalletState(wallet.GetStatus()),
|
||||||
|
Label: strings.TrimSpace(wallet.GetDescribable().GetName()),
|
||||||
|
OwnerRef: strings.TrimSpace(wallet.GetOwnerRef()),
|
||||||
|
ProviderDetails: details,
|
||||||
|
CreatedAt: wallet.GetCreatedAt(),
|
||||||
|
UpdatedAt: wallet.GetUpdatedAt(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainWalletState(status chainv1.ManagedWalletStatus) connectorv1.AccountState {
|
||||||
|
switch status {
|
||||||
|
case chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE:
|
||||||
|
return connectorv1.AccountState_ACCOUNT_ACTIVE
|
||||||
|
case chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED:
|
||||||
|
return connectorv1.AccountState_ACCOUNT_SUSPENDED
|
||||||
|
case chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED:
|
||||||
|
return connectorv1.AccountState_ACCOUNT_CLOSED
|
||||||
|
default:
|
||||||
|
return connectorv1.AccountState_ACCOUNT_STATE_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
|
||||||
|
if op == nil {
|
||||||
|
return nil, fmt.Errorf("transfer: operation is required")
|
||||||
|
}
|
||||||
|
if to := op.GetTo(); to != nil {
|
||||||
|
if account := to.GetAccount(); account != nil {
|
||||||
|
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(account.GetAccountId())}}, nil
|
||||||
|
}
|
||||||
|
if ext := to.GetExternal(); ext != nil {
|
||||||
|
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("transfer: to.account or to.external is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeMoneyForChain(m *moneyv1.Money) *moneyv1.Money {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
currency := strings.TrimSpace(m.GetCurrency())
|
||||||
|
if idx := strings.Index(currency, "-"); idx > 0 {
|
||||||
|
currency = currency[:idx]
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Amount: strings.TrimSpace(m.GetAmount()),
|
||||||
|
Currency: currency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseChainFees(reader params.Reader) []*chainv1.ServiceFeeBreakdown {
|
||||||
|
rawFees := reader.List("fees")
|
||||||
|
if len(rawFees) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]*chainv1.ServiceFeeBreakdown, 0, len(rawFees))
|
||||||
|
for _, item := range rawFees {
|
||||||
|
raw, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
||||||
|
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
||||||
|
if amount == "" || currency == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, &chainv1.ServiceFeeBreakdown{
|
||||||
|
FeeCode: strings.TrimSpace(fmt.Sprint(raw["fee_code"])),
|
||||||
|
Description: strings.TrimSpace(fmt.Sprint(raw["description"])),
|
||||||
|
Amount: &moneyv1.Money{Amount: amount, Currency: currency},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
|
||||||
|
if raw == nil {
|
||||||
|
return nil, fmt.Errorf("money is required")
|
||||||
|
}
|
||||||
|
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
||||||
|
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
||||||
|
if amount == "" || currency == "" {
|
||||||
|
return nil, fmt.Errorf("money is required")
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Amount: amount,
|
||||||
|
Currency: currency,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func feeEstimateResult(resp *chainv1.EstimateTransferFeeResponse) *structpb.Struct {
|
||||||
|
if resp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"estimation_context": strings.TrimSpace(resp.GetEstimationContext()),
|
||||||
|
}
|
||||||
|
if fee := resp.GetNetworkFee(); fee != nil {
|
||||||
|
payload["network_fee"] = map[string]interface{}{
|
||||||
|
"amount": strings.TrimSpace(fee.GetAmount()),
|
||||||
|
"currency": strings.TrimSpace(fee.GetCurrency()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, err := structpb.NewStruct(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func gasTopUpResult(amount *moneyv1.Money, capHit bool, transferRef string) *structpb.Struct {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"cap_hit": capHit,
|
||||||
|
}
|
||||||
|
if amount != nil {
|
||||||
|
payload["topup_amount"] = map[string]interface{}{
|
||||||
|
"amount": strings.TrimSpace(amount.GetAmount()),
|
||||||
|
"currency": strings.TrimSpace(amount.GetCurrency()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(transferRef) != "" {
|
||||||
|
payload["transfer_ref"] = strings.TrimSpace(transferRef)
|
||||||
|
}
|
||||||
|
result, err := structpb.NewStruct(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation {
|
||||||
|
if transfer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
op := &connectorv1.Operation{
|
||||||
|
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
|
||||||
|
Type: connectorv1.OperationType_TRANSFER,
|
||||||
|
Status: chainTransferStatusToOperation(transfer.GetStatus()),
|
||||||
|
Money: transfer.GetRequestedAmount(),
|
||||||
|
ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
|
||||||
|
CreatedAt: transfer.GetCreatedAt(),
|
||||||
|
UpdatedAt: transfer.GetUpdatedAt(),
|
||||||
|
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
|
||||||
|
ConnectorId: chainConnectorID,
|
||||||
|
AccountId: strings.TrimSpace(transfer.GetSourceWalletRef()),
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
if dest := transfer.GetDestination(); dest != nil {
|
||||||
|
switch d := dest.GetDestination().(type) {
|
||||||
|
case *chainv1.TransferDestination_ManagedWalletRef:
|
||||||
|
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
|
||||||
|
ConnectorId: chainConnectorID,
|
||||||
|
AccountId: strings.TrimSpace(d.ManagedWalletRef),
|
||||||
|
}}}
|
||||||
|
case *chainv1.TransferDestination_ExternalAddress:
|
||||||
|
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_External{External: &connectorv1.ExternalRef{
|
||||||
|
ExternalRef: strings.TrimSpace(d.ExternalAddress),
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainTransferStatusToOperation(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_PENDING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainStatusFromOperation(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_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseChainAsset(assetString string, reader params.Reader) (*chainv1.Asset, error) {
|
||||||
|
network := strings.TrimSpace(reader.String("network"))
|
||||||
|
token := strings.TrimSpace(reader.String("token_symbol"))
|
||||||
|
contract := strings.TrimSpace(reader.String("contract_address"))
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
token = tokenFromAssetString(assetString)
|
||||||
|
}
|
||||||
|
if network == "" {
|
||||||
|
network = networkFromAssetString(assetString)
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
return nil, fmt.Errorf("asset: token_symbol is required")
|
||||||
|
}
|
||||||
|
chain := shared.ChainEnumFromName(network)
|
||||||
|
if chain == chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED {
|
||||||
|
return nil, fmt.Errorf("asset: network is required")
|
||||||
|
}
|
||||||
|
return &chainv1.Asset{
|
||||||
|
Chain: chain,
|
||||||
|
TokenSymbol: strings.ToUpper(token),
|
||||||
|
ContractAddress: strings.ToLower(contract),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenFromAssetString(asset string) string {
|
||||||
|
if asset == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if idx := strings.Index(asset, "-"); idx > 0 {
|
||||||
|
return asset[:idx]
|
||||||
|
}
|
||||||
|
return asset
|
||||||
|
}
|
||||||
|
|
||||||
|
func networkFromAssetString(asset string) string {
|
||||||
|
if asset == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
idx := strings.Index(asset, "-")
|
||||||
|
if idx < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(asset[idx+1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func assetStringFromChainAsset(asset *chainv1.Asset) string {
|
||||||
|
if asset == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
symbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
||||||
|
if symbol == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
suffix := chainAssetSuffix(asset.GetChain())
|
||||||
|
if suffix == "" {
|
||||||
|
return symbol
|
||||||
|
}
|
||||||
|
return symbol + "-" + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainAssetSuffix(chain chainv1.ChainNetwork) string {
|
||||||
|
switch chain {
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET:
|
||||||
|
return "ETH"
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE:
|
||||||
|
return "ARB"
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET:
|
||||||
|
return "TRC20"
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE:
|
||||||
|
return "TRC20"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func describableFromLabel(label, desc string) *describablev1.Describable {
|
||||||
|
label = strings.TrimSpace(label)
|
||||||
|
desc = strings.TrimSpace(desc)
|
||||||
|
if label == "" && desc == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &describablev1.Describable{
|
||||||
|
Name: label,
|
||||||
|
Description: &desc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationAccountID(party *connectorv1.OperationParty) string {
|
||||||
|
if party == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if account := party.GetAccount(); account != nil {
|
||||||
|
return strings.TrimSpace(account.GetAccountId())
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
||||||
|
err := &connectorv1.ConnectorError{
|
||||||
|
Code: code,
|
||||||
|
Message: strings.TrimSpace(message),
|
||||||
|
AccountId: strings.TrimSpace(accountID),
|
||||||
|
}
|
||||||
|
if op != nil {
|
||||||
|
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
||||||
|
err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
|
||||||
|
err.OperationId = strings.TrimSpace(op.GetOperationId())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapErrorCode(err error) connectorv1.ErrorCode {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, merrors.ErrInvalidArg):
|
||||||
|
return connectorv1.ErrorCode_INVALID_PARAMS
|
||||||
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
return connectorv1.ErrorCode_NOT_FOUND
|
||||||
|
case errors.Is(err, merrors.ErrNotImplemented):
|
||||||
|
return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
|
||||||
|
case errors.Is(err, merrors.ErrInternal):
|
||||||
|
return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
|
||||||
|
default:
|
||||||
|
return connectorv1.ErrorCode_PROVIDER_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ import (
|
|||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ var (
|
|||||||
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
|
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service implements the UnifiedGatewayService RPC contract for chain operations.
|
// Service implements the ConnectorService RPC contract for chain operations.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
storage storage.Repository
|
storage storage.Repository
|
||||||
@@ -52,7 +52,7 @@ type Service struct {
|
|||||||
commands commands.Registry
|
commands commands.Registry
|
||||||
announcers []*discovery.Announcer
|
announcers []*discovery.Announcer
|
||||||
|
|
||||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
connectorv1.UnimplementedConnectorServiceServer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService constructs the chain gateway service skeleton.
|
// NewService constructs the chain gateway service skeleton.
|
||||||
@@ -95,7 +95,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
|||||||
// Register wires the service onto the provided gRPC router.
|
// Register wires the service onto the provided gRPC router.
|
||||||
func (s *Service) Register(router routers.GRPC) error {
|
func (s *Service) Register(router routers.GRPC) error {
|
||||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
connectorv1.RegisterConnectorServiceServer(reg, s)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client wraps the Monetix gateway gRPC API.
|
// Client wraps the Monetix gateway gRPC API.
|
||||||
@@ -22,9 +25,14 @@ type Client interface {
|
|||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type grpcConnectorClient interface {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
type gatewayClient struct {
|
type gatewayClient struct {
|
||||||
conn *grpc.ClientConn
|
conn *grpc.ClientConn
|
||||||
client unifiedv1.UnifiedGatewayServiceClient
|
client grpcConnectorClient
|
||||||
cfg Config
|
cfg Config
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
@@ -44,12 +52,12 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
|||||||
|
|
||||||
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
|
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, merrors.Internal("mntx: dial failed: " + err.Error())
|
return nil, merrors.Internal("mntx: dial failed: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &gatewayClient{
|
return &gatewayClient{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
client: unifiedv1.NewUnifiedGatewayServiceClient(conn),
|
client: connectorv1.NewConnectorServiceClient(conn),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
logger: cfg.Logger,
|
logger: cfg.Logger,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -70,37 +78,253 @@ func (g *gatewayClient) callContext(ctx context.Context, method string) (context
|
|||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = 5 * time.Second
|
timeout = 5 * time.Second
|
||||||
}
|
}
|
||||||
fields := []zap.Field{
|
if g.logger != nil {
|
||||||
zap.String("method", method),
|
fields := []zap.Field{
|
||||||
zap.Duration("timeout", timeout),
|
zap.String("method", method),
|
||||||
|
zap.Duration("timeout", timeout),
|
||||||
|
}
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
fields = append(fields, zap.Time("parent_deadline", deadline), zap.Duration("parent_deadline_in", time.Until(deadline)))
|
||||||
|
}
|
||||||
|
g.logger.Info("Mntx gateway client call timeout applied", fields...)
|
||||||
}
|
}
|
||||||
if deadline, ok := ctx.Deadline(); ok {
|
|
||||||
fields = append(fields, zap.Time("parent_deadline", deadline), zap.Duration("parent_deadline_in", time.Until(deadline)))
|
|
||||||
}
|
|
||||||
g.logger.Info("Mntx gateway client call timeout applied", fields...)
|
|
||||||
return context.WithTimeout(ctx, timeout)
|
return context.WithTimeout(ctx, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
||||||
ctx, cancel := g.callContext(ctx, "CreateCardPayout")
|
ctx, cancel := g.callContext(ctx, "CreateCardPayout")
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return g.client.CreateCardPayout(ctx, req)
|
operation, err := operationFromCardPayout(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := g.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 &mntxv1.CardPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), resp.GetReceipt())}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
||||||
ctx, cancel := g.callContext(ctx, "CreateCardTokenPayout")
|
ctx, cancel := g.callContext(ctx, "CreateCardTokenPayout")
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return g.client.CreateCardTokenPayout(ctx, req)
|
operation, err := operationFromTokenPayout(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := g.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 &mntxv1.CardTokenPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), resp.GetReceipt())}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gatewayClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
|
func (g *gatewayClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
|
||||||
ctx, cancel := g.callContext(ctx, "GetCardPayoutStatus")
|
ctx, cancel := g.callContext(ctx, "GetCardPayoutStatus")
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return g.client.GetCardPayoutStatus(ctx, req)
|
if req == nil || strings.TrimSpace(req.GetPayoutId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("mntx: payout_id is required")
|
||||||
|
}
|
||||||
|
resp, err := g.client.GetOperation(ctx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(req.GetPayoutId())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mntxv1.GetCardPayoutStatusResponse{Payout: payoutFromOperation(resp.GetOperation())}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gatewayClient) ListGatewayInstances(ctx context.Context, req *mntxv1.ListGatewayInstancesRequest) (*mntxv1.ListGatewayInstancesResponse, error) {
|
func (g *gatewayClient) ListGatewayInstances(ctx context.Context, req *mntxv1.ListGatewayInstancesRequest) (*mntxv1.ListGatewayInstancesResponse, error) {
|
||||||
ctx, cancel := g.callContext(ctx, "ListGatewayInstances")
|
return nil, merrors.NotImplemented("mntx: ListGatewayInstances not supported via connector")
|
||||||
defer cancel()
|
}
|
||||||
return g.client.ListGatewayInstances(ctx, req)
|
|
||||||
|
func operationFromCardPayout(req *mntxv1.CardPayoutRequest) (*connectorv1.Operation, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, merrors.InvalidArgument("mntx: request is required")
|
||||||
|
}
|
||||||
|
params := payoutParamsFromCard(req)
|
||||||
|
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
||||||
|
return &connectorv1.Operation{
|
||||||
|
Type: connectorv1.OperationType_PAYOUT,
|
||||||
|
IdempotencyKey: strings.TrimSpace(req.GetPayoutId()),
|
||||||
|
Money: money,
|
||||||
|
Params: structFromMap(params),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationFromTokenPayout(req *mntxv1.CardTokenPayoutRequest) (*connectorv1.Operation, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, merrors.InvalidArgument("mntx: request is required")
|
||||||
|
}
|
||||||
|
params := payoutParamsFromToken(req)
|
||||||
|
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
||||||
|
return &connectorv1.Operation{
|
||||||
|
Type: connectorv1.OperationType_PAYOUT,
|
||||||
|
IdempotencyKey: strings.TrimSpace(req.GetPayoutId()),
|
||||||
|
Money: money,
|
||||||
|
Params: structFromMap(params),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{} {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"payout_id": strings.TrimSpace(req.GetPayoutId()),
|
||||||
|
"project_id": req.GetProjectId(),
|
||||||
|
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
||||||
|
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
||||||
|
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
|
||||||
|
"customer_last_name": strings.TrimSpace(req.GetCustomerLastName()),
|
||||||
|
"customer_ip": strings.TrimSpace(req.GetCustomerIp()),
|
||||||
|
"customer_zip": strings.TrimSpace(req.GetCustomerZip()),
|
||||||
|
"customer_country": strings.TrimSpace(req.GetCustomerCountry()),
|
||||||
|
"customer_state": strings.TrimSpace(req.GetCustomerState()),
|
||||||
|
"customer_city": strings.TrimSpace(req.GetCustomerCity()),
|
||||||
|
"customer_address": strings.TrimSpace(req.GetCustomerAddress()),
|
||||||
|
"amount_minor": req.GetAmountMinor(),
|
||||||
|
"currency": strings.TrimSpace(req.GetCurrency()),
|
||||||
|
"card_pan": strings.TrimSpace(req.GetCardPan()),
|
||||||
|
"card_exp_year": req.GetCardExpYear(),
|
||||||
|
"card_exp_month": req.GetCardExpMonth(),
|
||||||
|
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
||||||
|
}
|
||||||
|
if len(req.GetMetadata()) > 0 {
|
||||||
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interface{} {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"payout_id": strings.TrimSpace(req.GetPayoutId()),
|
||||||
|
"project_id": req.GetProjectId(),
|
||||||
|
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
||||||
|
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
||||||
|
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
|
||||||
|
"customer_last_name": strings.TrimSpace(req.GetCustomerLastName()),
|
||||||
|
"customer_ip": strings.TrimSpace(req.GetCustomerIp()),
|
||||||
|
"customer_zip": strings.TrimSpace(req.GetCustomerZip()),
|
||||||
|
"customer_country": strings.TrimSpace(req.GetCustomerCountry()),
|
||||||
|
"customer_state": strings.TrimSpace(req.GetCustomerState()),
|
||||||
|
"customer_city": strings.TrimSpace(req.GetCustomerCity()),
|
||||||
|
"customer_address": strings.TrimSpace(req.GetCustomerAddress()),
|
||||||
|
"amount_minor": req.GetAmountMinor(),
|
||||||
|
"currency": strings.TrimSpace(req.GetCurrency()),
|
||||||
|
"card_token": strings.TrimSpace(req.GetCardToken()),
|
||||||
|
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
||||||
|
"masked_pan": strings.TrimSpace(req.GetMaskedPan()),
|
||||||
|
}
|
||||||
|
if len(req.GetMetadata()) > 0 {
|
||||||
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func moneyFromMinor(amount int64, currency string) *moneyv1.Money {
|
||||||
|
if amount <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dec := decimal.NewFromInt(amount).Div(decimal.NewFromInt(100))
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Amount: dec.StringFixed(2),
|
||||||
|
Currency: strings.ToUpper(strings.TrimSpace(currency)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutFromReceipt(payoutID string, receipt *connectorv1.OperationReceipt) *mntxv1.CardPayoutState {
|
||||||
|
state := &mntxv1.CardPayoutState{PayoutId: strings.TrimSpace(payoutID)}
|
||||||
|
if receipt == nil {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
state.Status = payoutStatusFromOperation(receipt.GetStatus())
|
||||||
|
state.ProviderPaymentId = strings.TrimSpace(receipt.GetProviderRef())
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutFromOperation(op *connectorv1.Operation) *mntxv1.CardPayoutState {
|
||||||
|
if op == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
state := &mntxv1.CardPayoutState{
|
||||||
|
PayoutId: strings.TrimSpace(op.GetOperationId()),
|
||||||
|
Status: payoutStatusFromOperation(op.GetStatus()),
|
||||||
|
ProviderPaymentId: strings.TrimSpace(op.GetProviderRef()),
|
||||||
|
}
|
||||||
|
if money := op.GetMoney(); money != nil {
|
||||||
|
state.Currency = strings.TrimSpace(money.GetCurrency())
|
||||||
|
state.AmountMinor = minorFromMoney(money)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func minorFromMoney(m *moneyv1.Money) int64 {
|
||||||
|
if m == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
amount := strings.TrimSpace(m.GetAmount())
|
||||||
|
if amount == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
dec, err := decimal.NewFromString(amount)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return dec.Mul(decimal.NewFromInt(100)).IntPart()
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutStatusFromOperation(status connectorv1.OperationStatus) mntxv1.PayoutStatus {
|
||||||
|
switch status {
|
||||||
|
case connectorv1.OperationStatus_CONFIRMED:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED
|
||||||
|
case connectorv1.OperationStatus_FAILED:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||||
|
case connectorv1.OperationStatus_PENDING, connectorv1.OperationStatus_SUBMITTED:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
||||||
|
default:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ replace github.com/tech/sendico/pkg => ../../pkg
|
|||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
|
github.com/shopspring/decimal v1.4.0
|
||||||
github.com/tech/sendico/pkg v0.1.0
|
github.com/tech/sendico/pkg v0.1.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
google.golang.org/grpc v1.78.0
|
google.golang.org/grpc v1.78.0
|
||||||
|
|||||||
294
api/gateway/mntx/internal/service/gateway/connector.go
Normal file
294
api/gateway/mntx/internal/service/gateway/connector.go
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
||||||
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mntxConnectorID = "mntx"
|
||||||
|
|
||||||
|
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||||
|
return &connectorv1.GetCapabilitiesResponse{
|
||||||
|
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||||
|
ConnectorType: mntxConnectorID,
|
||||||
|
Version: appversion.Create().Short(),
|
||||||
|
SupportedAccountKinds: nil,
|
||||||
|
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_PAYOUT},
|
||||||
|
OperationParams: mntxOperationParams(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) OpenAccount(_ context.Context, _ *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported", nil, "")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAccount(_ context.Context, _ *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
|
||||||
|
return nil, merrors.NotImplemented("get_account: unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListAccounts(_ context.Context, _ *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
|
||||||
|
return nil, merrors.NotImplemented("list_accounts: unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetBalance(_ context.Context, _ *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
|
||||||
|
return nil, merrors.NotImplemented("get_balance: unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
||||||
|
if req == nil || req.GetOperation() == nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil
|
||||||
|
}
|
||||||
|
op := req.GetOperation()
|
||||||
|
if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
if op.GetType() != connectorv1.OperationType_PAYOUT {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
|
||||||
|
}
|
||||||
|
reader := params.New(op.GetParams())
|
||||||
|
amountMinor, currency, err := payoutAmount(op, reader)
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payoutID := strings.TrimSpace(reader.String("payout_id"))
|
||||||
|
if payoutID == "" {
|
||||||
|
payoutID = strings.TrimSpace(op.GetIdempotencyKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(reader.String("card_token")) != "" {
|
||||||
|
resp, err := s.CreateCardTokenPayout(ctx, buildCardTokenPayoutRequest(reader, payoutID, amountMinor, currency))
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
|
||||||
|
}
|
||||||
|
resp, err := s.CreateCardPayout(ctx, buildCardPayoutRequest(reader, payoutID, amountMinor, currency))
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
|
||||||
|
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
|
||||||
|
}
|
||||||
|
resp, err := s.GetCardPayoutStatus(ctx, &mntxv1.GetCardPayoutStatusRequest{PayoutId: strings.TrimSpace(req.GetOperationId())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &connectorv1.GetOperationResponse{Operation: payoutToOperation(resp.GetPayout())}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListOperations(_ context.Context, _ *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
|
||||||
|
return nil, merrors.NotImplemented("list_operations: unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mntxOperationParams() []*connectorv1.OperationParamSpec {
|
||||||
|
return []*connectorv1.OperationParamSpec{
|
||||||
|
{OperationType: connectorv1.OperationType_PAYOUT, Params: []*connectorv1.ParamSpec{
|
||||||
|
{Key: "customer_id", Type: connectorv1.ParamType_STRING, Required: true},
|
||||||
|
{Key: "customer_first_name", Type: connectorv1.ParamType_STRING, Required: true},
|
||||||
|
{Key: "customer_last_name", Type: connectorv1.ParamType_STRING, Required: true},
|
||||||
|
{Key: "customer_ip", Type: connectorv1.ParamType_STRING, Required: true},
|
||||||
|
{Key: "card_token", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "card_pan", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "card_exp_year", Type: connectorv1.ParamType_INT, Required: false},
|
||||||
|
{Key: "card_exp_month", Type: connectorv1.ParamType_INT, Required: false},
|
||||||
|
{Key: "card_holder", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "amount_minor", Type: connectorv1.ParamType_INT, Required: false},
|
||||||
|
{Key: "project_id", Type: connectorv1.ParamType_INT, Required: false},
|
||||||
|
{Key: "customer_middle_name", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "customer_country", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "customer_state", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "customer_city", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "customer_address", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "customer_zip", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "masked_pan", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutAmount(op *connectorv1.Operation, reader params.Reader) (int64, string, error) {
|
||||||
|
if op == nil {
|
||||||
|
return 0, "", fmt.Errorf("payout: operation is required")
|
||||||
|
}
|
||||||
|
currency := currencyFromOperation(op)
|
||||||
|
if currency == "" {
|
||||||
|
return 0, "", fmt.Errorf("payout: currency is required")
|
||||||
|
}
|
||||||
|
if minor, ok := reader.Int64("amount_minor"); ok && minor > 0 {
|
||||||
|
return minor, currency, nil
|
||||||
|
}
|
||||||
|
money := op.GetMoney()
|
||||||
|
if money == nil {
|
||||||
|
return 0, "", fmt.Errorf("payout: money is required")
|
||||||
|
}
|
||||||
|
amount := strings.TrimSpace(money.GetAmount())
|
||||||
|
if amount == "" {
|
||||||
|
return 0, "", fmt.Errorf("payout: amount is required")
|
||||||
|
}
|
||||||
|
dec, err := decimal.NewFromString(amount)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", fmt.Errorf("payout: invalid amount")
|
||||||
|
}
|
||||||
|
minor := dec.Mul(decimal.NewFromInt(100)).IntPart()
|
||||||
|
return minor, currency, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func currencyFromOperation(op *connectorv1.Operation) string {
|
||||||
|
if op == nil || op.GetMoney() == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
currency := strings.TrimSpace(op.GetMoney().GetCurrency())
|
||||||
|
if idx := strings.Index(currency, "-"); idx > 0 {
|
||||||
|
currency = currency[:idx]
|
||||||
|
}
|
||||||
|
return strings.ToUpper(currency)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCardTokenPayoutRequest(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
|
||||||
|
req := &mntxv1.CardTokenPayoutRequest{
|
||||||
|
PayoutId: payoutID,
|
||||||
|
ProjectId: readerInt64(reader, "project_id"),
|
||||||
|
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||||
|
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||||
|
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
||||||
|
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
||||||
|
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
||||||
|
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
||||||
|
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
||||||
|
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
||||||
|
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
||||||
|
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
||||||
|
AmountMinor: amountMinor,
|
||||||
|
Currency: currency,
|
||||||
|
CardToken: strings.TrimSpace(reader.String("card_token")),
|
||||||
|
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||||
|
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
|
||||||
|
Metadata: reader.StringMap("metadata"),
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCardPayoutRequest(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
||||||
|
return &mntxv1.CardPayoutRequest{
|
||||||
|
PayoutId: payoutID,
|
||||||
|
ProjectId: readerInt64(reader, "project_id"),
|
||||||
|
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||||
|
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||||
|
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
||||||
|
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
||||||
|
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
||||||
|
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
||||||
|
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
||||||
|
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
||||||
|
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
||||||
|
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
||||||
|
AmountMinor: amountMinor,
|
||||||
|
Currency: currency,
|
||||||
|
CardPan: strings.TrimSpace(reader.String("card_pan")),
|
||||||
|
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
|
||||||
|
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
|
||||||
|
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||||
|
Metadata: reader.StringMap("metadata"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readerInt64(reader params.Reader, key string) int64 {
|
||||||
|
if v, ok := reader.Int64(key); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutReceipt(state *mntxv1.CardPayoutState) *connectorv1.OperationReceipt {
|
||||||
|
if state == nil {
|
||||||
|
return &connectorv1.OperationReceipt{
|
||||||
|
Status: connectorv1.OperationStatus_PENDING,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &connectorv1.OperationReceipt{
|
||||||
|
OperationId: strings.TrimSpace(state.GetPayoutId()),
|
||||||
|
Status: payoutStatusToOperation(state.GetStatus()),
|
||||||
|
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutToOperation(state *mntxv1.CardPayoutState) *connectorv1.Operation {
|
||||||
|
if state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &connectorv1.Operation{
|
||||||
|
OperationId: strings.TrimSpace(state.GetPayoutId()),
|
||||||
|
Type: connectorv1.OperationType_PAYOUT,
|
||||||
|
Status: payoutStatusToOperation(state.GetStatus()),
|
||||||
|
Money: &moneyv1.Money{
|
||||||
|
Amount: minorToDecimal(state.GetAmountMinor()),
|
||||||
|
Currency: strings.ToUpper(strings.TrimSpace(state.GetCurrency())),
|
||||||
|
},
|
||||||
|
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
|
||||||
|
CreatedAt: state.GetCreatedAt(),
|
||||||
|
UpdatedAt: state.GetUpdatedAt(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func minorToDecimal(amount int64) string {
|
||||||
|
dec := decimal.NewFromInt(amount).Div(decimal.NewFromInt(100))
|
||||||
|
return dec.StringFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutStatusToOperation(status mntxv1.PayoutStatus) connectorv1.OperationStatus {
|
||||||
|
switch status {
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED:
|
||||||
|
return connectorv1.OperationStatus_CONFIRMED
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
|
||||||
|
return connectorv1.OperationStatus_FAILED
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING:
|
||||||
|
return connectorv1.OperationStatus_PENDING
|
||||||
|
default:
|
||||||
|
return connectorv1.OperationStatus_PENDING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
||||||
|
err := &connectorv1.ConnectorError{
|
||||||
|
Code: code,
|
||||||
|
Message: strings.TrimSpace(message),
|
||||||
|
AccountId: strings.TrimSpace(accountID),
|
||||||
|
}
|
||||||
|
if op != nil {
|
||||||
|
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
||||||
|
err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
|
||||||
|
err.OperationId = strings.TrimSpace(op.GetOperationId())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapErrorCode(err error) connectorv1.ErrorCode {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, merrors.ErrInvalidArg):
|
||||||
|
return connectorv1.ErrorCode_INVALID_PARAMS
|
||||||
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
return connectorv1.ErrorCode_NOT_FOUND
|
||||||
|
case errors.Is(err, merrors.ErrNotImplemented):
|
||||||
|
return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
|
||||||
|
case errors.Is(err, merrors.ErrInternal):
|
||||||
|
return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
|
||||||
|
default:
|
||||||
|
return connectorv1.ErrorCode_PROVIDER_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
@@ -31,7 +31,7 @@ type Service struct {
|
|||||||
gatewayDescriptor *gatewayv1.GatewayInstanceDescriptor
|
gatewayDescriptor *gatewayv1.GatewayInstanceDescriptor
|
||||||
announcer *discovery.Announcer
|
announcer *discovery.Announcer
|
||||||
|
|
||||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
connectorv1.UnimplementedConnectorServiceServer
|
||||||
}
|
}
|
||||||
|
|
||||||
type payoutFailure interface {
|
type payoutFailure interface {
|
||||||
@@ -96,7 +96,7 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
|||||||
// Register wires the service onto the provided gRPC router.
|
// Register wires the service onto the provided gRPC router.
|
||||||
func (s *Service) Register(router routers.GRPC) error {
|
func (s *Service) Register(router routers.GRPC) error {
|
||||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
connectorv1.RegisterConnectorServiceServer(reg, s)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
254
api/gateway/tgsettle/internal/service/gateway/connector.go
Normal file
254
api/gateway/tgsettle/internal/service/gateway/connector.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tgsettleConnectorID = "tgsettle"
|
||||||
|
|
||||||
|
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||||
|
return &connectorv1.GetCapabilitiesResponse{
|
||||||
|
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||||
|
ConnectorType: tgsettleConnectorID,
|
||||||
|
Version: "",
|
||||||
|
SupportedAccountKinds: nil,
|
||||||
|
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_TRANSFER},
|
||||||
|
OperationParams: tgsettleOperationParams(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) OpenAccount(_ context.Context, _ *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported", nil, "")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAccount(_ context.Context, _ *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
|
||||||
|
return nil, merrors.NotImplemented("get_account: unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListAccounts(_ context.Context, _ *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
|
||||||
|
return nil, merrors.NotImplemented("list_accounts: unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetBalance(_ context.Context, _ *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
|
||||||
|
return nil, merrors.NotImplemented("get_balance: unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
||||||
|
if req == nil || req.GetOperation() == nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil
|
||||||
|
}
|
||||||
|
op := req.GetOperation()
|
||||||
|
if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
if op.GetType() != connectorv1.OperationType_TRANSFER {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
|
||||||
|
}
|
||||||
|
reader := params.New(op.GetParams())
|
||||||
|
paymentIntentID := strings.TrimSpace(reader.String("payment_intent_id"))
|
||||||
|
if paymentIntentID == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: payment_intent_id is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
source := operationAccountID(op.GetFrom())
|
||||||
|
if source == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
dest, err := transferDestinationFromOperation(op)
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
amount := op.GetMoney()
|
||||||
|
if amount == nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := reader.StringMap("metadata")
|
||||||
|
if metadata == nil {
|
||||||
|
metadata = map[string]string{}
|
||||||
|
}
|
||||||
|
metadata[metadataPaymentIntentID] = paymentIntentID
|
||||||
|
if quoteRef := strings.TrimSpace(reader.String("quote_ref")); quoteRef != "" {
|
||||||
|
metadata[metadataQuoteRef] = quoteRef
|
||||||
|
}
|
||||||
|
if targetChatID := strings.TrimSpace(reader.String("target_chat_id")); targetChatID != "" {
|
||||||
|
metadata[metadataTargetChatID] = targetChatID
|
||||||
|
}
|
||||||
|
if outgoingLeg := strings.TrimSpace(reader.String("outgoing_leg")); outgoingLeg != "" {
|
||||||
|
metadata[metadataOutgoingLeg] = outgoingLeg
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
|
||||||
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
OrganizationRef: strings.TrimSpace(reader.String("organization_ref")),
|
||||||
|
SourceWalletRef: source,
|
||||||
|
Destination: dest,
|
||||||
|
Amount: normalizeMoneyForTransfer(amount),
|
||||||
|
Metadata: metadata,
|
||||||
|
ClientReference: paymentIntentID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
transfer := resp.GetTransfer()
|
||||||
|
return &connectorv1.SubmitOperationResponse{
|
||||||
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
|
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
|
||||||
|
Status: transferStatusToOperation(transfer.GetStatus()),
|
||||||
|
ProviderRef: strings.TrimSpace(transfer.GetTransferRef()),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
|
||||||
|
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
|
||||||
|
}
|
||||||
|
resp, err := s.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: strings.TrimSpace(req.GetOperationId())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &connectorv1.GetOperationResponse{Operation: transferToOperation(resp.GetTransfer())}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListOperations(_ context.Context, _ *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
|
||||||
|
return nil, merrors.NotImplemented("list_operations: unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func tgsettleOperationParams() []*connectorv1.OperationParamSpec {
|
||||||
|
return []*connectorv1.OperationParamSpec{
|
||||||
|
{OperationType: connectorv1.OperationType_TRANSFER, Params: []*connectorv1.ParamSpec{
|
||||||
|
{Key: "payment_intent_id", Type: connectorv1.ParamType_STRING, Required: true},
|
||||||
|
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "quote_ref", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "target_chat_id", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "outgoing_leg", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
|
||||||
|
if op == nil {
|
||||||
|
return nil, fmt.Errorf("transfer: operation is required")
|
||||||
|
}
|
||||||
|
if to := op.GetTo(); to != nil {
|
||||||
|
if account := to.GetAccount(); account != nil {
|
||||||
|
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(account.GetAccountId())}}, nil
|
||||||
|
}
|
||||||
|
if ext := to.GetExternal(); ext != nil {
|
||||||
|
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("transfer: to.account or to.external is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeMoneyForTransfer(m *moneyv1.Money) *moneyv1.Money {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
currency := strings.TrimSpace(m.GetCurrency())
|
||||||
|
if idx := strings.Index(currency, "-"); idx > 0 {
|
||||||
|
currency = currency[:idx]
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Amount: strings.TrimSpace(m.GetAmount()),
|
||||||
|
Currency: currency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation {
|
||||||
|
if transfer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
op := &connectorv1.Operation{
|
||||||
|
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
|
||||||
|
Type: connectorv1.OperationType_TRANSFER,
|
||||||
|
Status: transferStatusToOperation(transfer.GetStatus()),
|
||||||
|
Money: transfer.GetRequestedAmount(),
|
||||||
|
ProviderRef: strings.TrimSpace(transfer.GetTransferRef()),
|
||||||
|
CreatedAt: transfer.GetCreatedAt(),
|
||||||
|
UpdatedAt: transfer.GetUpdatedAt(),
|
||||||
|
}
|
||||||
|
if source := strings.TrimSpace(transfer.GetSourceWalletRef()); source != "" {
|
||||||
|
op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
|
||||||
|
ConnectorId: tgsettleConnectorID,
|
||||||
|
AccountId: source,
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
if dest := transfer.GetDestination(); dest != nil {
|
||||||
|
switch d := dest.GetDestination().(type) {
|
||||||
|
case *chainv1.TransferDestination_ManagedWalletRef:
|
||||||
|
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
|
||||||
|
ConnectorId: tgsettleConnectorID,
|
||||||
|
AccountId: strings.TrimSpace(d.ManagedWalletRef),
|
||||||
|
}}}
|
||||||
|
case *chainv1.TransferDestination_ExternalAddress:
|
||||||
|
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_External{External: &connectorv1.ExternalRef{
|
||||||
|
ExternalRef: strings.TrimSpace(d.ExternalAddress),
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferStatusToOperation(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_PENDING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationAccountID(party *connectorv1.OperationParty) string {
|
||||||
|
if party == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if account := party.GetAccount(); account != nil {
|
||||||
|
return strings.TrimSpace(account.GetAccountId())
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
||||||
|
err := &connectorv1.ConnectorError{
|
||||||
|
Code: code,
|
||||||
|
Message: strings.TrimSpace(message),
|
||||||
|
AccountId: strings.TrimSpace(accountID),
|
||||||
|
}
|
||||||
|
if op != nil {
|
||||||
|
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
||||||
|
err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
|
||||||
|
err.OperationId = strings.TrimSpace(op.GetOperationId())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapErrorCode(err error) connectorv1.ErrorCode {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, merrors.ErrInvalidArg):
|
||||||
|
return connectorv1.ErrorCode_INVALID_PARAMS
|
||||||
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
return connectorv1.ErrorCode_NOT_FOUND
|
||||||
|
case errors.Is(err, merrors.ErrNotImplemented):
|
||||||
|
return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
|
||||||
|
case errors.Is(err, merrors.ErrInternal):
|
||||||
|
return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
|
||||||
|
default:
|
||||||
|
return connectorv1.ErrorCode_PROVIDER_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,10 +20,10 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
|
||||||
"github.com/tech/sendico/pkg/server/grpcapp"
|
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -65,7 +65,7 @@ type Service struct {
|
|||||||
pending map[string]*model.PaymentGatewayIntent
|
pending map[string]*model.PaymentGatewayIntent
|
||||||
consumers []msg.Consumer
|
consumers []msg.Consumer
|
||||||
|
|
||||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
connectorv1.UnimplementedConnectorServiceServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, broker mb.Broker, cfg Config) *Service {
|
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, broker mb.Broker, cfg Config) *Service {
|
||||||
@@ -89,7 +89,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
|||||||
|
|
||||||
func (s *Service) Register(router routers.GRPC) error {
|
func (s *Service) Register(router routers.GRPC) error {
|
||||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
connectorv1.RegisterConnectorServiceServer(reg, s)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,19 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ledgerConnectorID = "ledger"
|
||||||
|
|
||||||
// Client exposes typed helpers around the ledger gRPC API.
|
// Client exposes typed helpers around the ledger gRPC API.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
ReadBalance(ctx context.Context, accountID string) (*moneyv1.Money, error)
|
ReadBalance(ctx context.Context, accountID string) (*moneyv1.Money, error)
|
||||||
@@ -37,22 +42,20 @@ type Client interface {
|
|||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type grpcLedgerClient interface {
|
type grpcConnectorClient interface {
|
||||||
CreateAccount(ctx context.Context, in *ledgerv1.CreateAccountRequest, opts ...grpc.CallOption) (*ledgerv1.CreateAccountResponse, error)
|
OpenAccount(ctx context.Context, in *connectorv1.OpenAccountRequest, opts ...grpc.CallOption) (*connectorv1.OpenAccountResponse, error)
|
||||||
ListAccounts(ctx context.Context, in *ledgerv1.ListAccountsRequest, opts ...grpc.CallOption) (*ledgerv1.ListAccountsResponse, error)
|
GetAccount(ctx context.Context, in *connectorv1.GetAccountRequest, opts ...grpc.CallOption) (*connectorv1.GetAccountResponse, error)
|
||||||
PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
ListAccounts(ctx context.Context, in *connectorv1.ListAccountsRequest, opts ...grpc.CallOption) (*connectorv1.ListAccountsResponse, error)
|
||||||
PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
GetBalance(ctx context.Context, in *connectorv1.GetBalanceRequest, opts ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error)
|
||||||
TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
|
||||||
ApplyFXWithCharges(ctx context.Context, in *ledgerv1.FXRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error)
|
||||||
GetBalance(ctx context.Context, in *ledgerv1.GetBalanceRequest, opts ...grpc.CallOption) (*ledgerv1.BalanceResponse, error)
|
ListOperations(ctx context.Context, in *connectorv1.ListOperationsRequest, opts ...grpc.CallOption) (*connectorv1.ListOperationsResponse, error)
|
||||||
GetJournalEntry(ctx context.Context, in *ledgerv1.GetEntryRequest, opts ...grpc.CallOption) (*ledgerv1.JournalEntryResponse, error)
|
|
||||||
GetStatement(ctx context.Context, in *ledgerv1.GetStatementRequest, opts ...grpc.CallOption) (*ledgerv1.StatementResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ledgerClient struct {
|
type ledgerClient struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
conn *grpc.ClientConn
|
conn *grpc.ClientConn
|
||||||
client grpcLedgerClient
|
client grpcConnectorClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// New dials the ledger endpoint and returns a ready client.
|
// New dials the ledger endpoint and returns a ready client.
|
||||||
@@ -82,12 +85,12 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
|||||||
return &ledgerClient{
|
return &ledgerClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
client: unifiedv1.NewUnifiedGatewayServiceClient(conn),
|
client: connectorv1.NewConnectorServiceClient(conn),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWithClient injects a pre-built ledger client (useful for tests).
|
// NewWithClient injects a pre-built ledger client (useful for tests).
|
||||||
func NewWithClient(cfg Config, lc grpcLedgerClient) Client {
|
func NewWithClient(cfg Config, lc grpcConnectorClient) Client {
|
||||||
cfg.setDefaults()
|
cfg.setDefaults()
|
||||||
return &ledgerClient{
|
return &ledgerClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -179,55 +182,460 @@ func (c *ledgerClient) HoldBalance(ctx context.Context, accountID string, amount
|
|||||||
func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.CreateAccount(ctx, req)
|
if req == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: request is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.GetCurrency()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: currency is required")
|
||||||
|
}
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||||
|
"account_code": strings.TrimSpace(req.GetAccountCode()),
|
||||||
|
"account_type": req.GetAccountType().String(),
|
||||||
|
"status": req.GetStatus().String(),
|
||||||
|
"allow_negative": req.GetAllowNegative(),
|
||||||
|
"is_settlement": req.GetIsSettlement(),
|
||||||
|
}
|
||||||
|
if len(req.GetMetadata()) > 0 {
|
||||||
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||||
|
}
|
||||||
|
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
|
||||||
|
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
|
||||||
|
Asset: strings.TrimSpace(req.GetCurrency()),
|
||||||
|
Params: structFromMap(params),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.GetError() != nil {
|
||||||
|
return nil, connectorError(resp.GetError())
|
||||||
|
}
|
||||||
|
return &ledgerv1.CreateAccountResponse{Account: ledgerAccountFromConnector(resp.GetAccount())}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
func (c *ledgerClient) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ListAccounts(ctx, req)
|
if req == nil || strings.TrimSpace(req.GetOrganizationRef()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: organization_ref is required")
|
||||||
|
}
|
||||||
|
resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{OwnerRef: strings.TrimSpace(req.GetOrganizationRef())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
accounts := make([]*ledgerv1.LedgerAccount, 0, len(resp.GetAccounts()))
|
||||||
|
for _, account := range resp.GetAccounts() {
|
||||||
|
accounts = append(accounts, ledgerAccountFromConnector(account))
|
||||||
|
}
|
||||||
|
return &ledgerv1.ListAccountsResponse{Accounts: accounts}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
return c.submitLedgerOperation(ctx, connectorv1.OperationType_CREDIT, "", req.GetLedgerAccountRef(), req.GetMoney(), req)
|
||||||
defer cancel()
|
|
||||||
return c.client.PostCreditWithCharges(ctx, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
func (c *ledgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
return c.submitLedgerOperation(ctx, connectorv1.OperationType_DEBIT, req.GetLedgerAccountRef(), "", req.GetMoney(), req)
|
||||||
defer cancel()
|
|
||||||
return c.client.PostDebitWithCharges(ctx, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
func (c *ledgerClient) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
return c.submitLedgerOperation(ctx, connectorv1.OperationType_TRANSFER, req.GetFromLedgerAccountRef(), req.GetToLedgerAccountRef(), req.GetMoney(), req)
|
||||||
defer cancel()
|
|
||||||
return c.client.TransferInternal(ctx, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
|
func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ApplyFXWithCharges(ctx, req)
|
if req == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: request is required")
|
||||||
|
}
|
||||||
|
if req.GetFromMoney() == nil || req.GetToMoney() == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: from_money and to_money are required")
|
||||||
|
}
|
||||||
|
params := ledgerOperationParams(req.GetOrganizationRef(), req.GetDescription(), req.GetMetadata(), req.GetCharges(), req.GetEventTime())
|
||||||
|
params["rate"] = strings.TrimSpace(req.GetRate())
|
||||||
|
params["to_money"] = map[string]interface{}{"amount": req.GetToMoney().GetAmount(), "currency": req.GetToMoney().GetCurrency()}
|
||||||
|
operation := &connectorv1.Operation{
|
||||||
|
Type: connectorv1.OperationType_FX,
|
||||||
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||||
|
From: accountParty(req.GetFromLedgerAccountRef()),
|
||||||
|
To: accountParty(req.GetToLedgerAccountRef()),
|
||||||
|
Money: req.GetFromMoney(),
|
||||||
|
Params: structFromMap(params),
|
||||||
|
}
|
||||||
|
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 &ledgerv1.PostResponse{JournalEntryRef: resp.GetReceipt().GetOperationId(), EntryType: ledgerv1.EntryType_ENTRY_FX}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) {
|
func (c *ledgerClient) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetBalance(ctx, req)
|
if req == nil || strings.TrimSpace(req.GetLedgerAccountRef()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: ledger_account_ref is required")
|
||||||
|
}
|
||||||
|
resp, err := c.client.GetBalance(ctx, &connectorv1.GetBalanceRequest{AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
balance := resp.GetBalance()
|
||||||
|
if balance == nil {
|
||||||
|
return nil, merrors.Internal("ledger: balance response missing")
|
||||||
|
}
|
||||||
|
return &ledgerv1.BalanceResponse{
|
||||||
|
LedgerAccountRef: strings.TrimSpace(req.GetLedgerAccountRef()),
|
||||||
|
Balance: balance.GetAvailable(),
|
||||||
|
LastUpdated: balance.GetCalculatedAt(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) {
|
func (c *ledgerClient) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetJournalEntry(ctx, req)
|
if req == nil || strings.TrimSpace(req.GetEntryRef()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: entry_ref is required")
|
||||||
|
}
|
||||||
|
resp, err := c.client.GetOperation(ctx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(req.GetEntryRef())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return journalEntryFromOperation(resp.GetOperation()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) {
|
func (c *ledgerClient) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetStatement(ctx, req)
|
if req == nil || strings.TrimSpace(req.GetLedgerAccountRef()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: ledger_account_ref is required")
|
||||||
|
}
|
||||||
|
resp, err := c.client.ListOperations(ctx, &connectorv1.ListOperationsRequest{
|
||||||
|
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
|
||||||
|
Page: pageFromStatement(req),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries := make([]*ledgerv1.JournalEntryResponse, 0, len(resp.GetOperations()))
|
||||||
|
for _, op := range resp.GetOperations() {
|
||||||
|
entries = append(entries, journalEntryFromOperation(op))
|
||||||
|
}
|
||||||
|
nextCursor := ""
|
||||||
|
if resp.GetPage() != nil {
|
||||||
|
nextCursor = resp.GetPage().GetNextCursor()
|
||||||
|
}
|
||||||
|
return &ledgerv1.StatementResponse{Entries: entries, NextCursor: nextCursor}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connectorv1.OperationType, fromRef, toRef string, money *moneyv1.Money, req interface{}) (*ledgerv1.PostResponse, error) {
|
||||||
|
ctx, cancel := c.callContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
if money == nil {
|
||||||
|
return nil, merrors.InvalidArgument("ledger: money is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
idempotencyKey string
|
||||||
|
orgRef string
|
||||||
|
description string
|
||||||
|
metadata map[string]string
|
||||||
|
charges []*ledgerv1.PostingLine
|
||||||
|
eventTime *timestamppb.Timestamp
|
||||||
|
contraRef string
|
||||||
|
)
|
||||||
|
|
||||||
|
switch r := req.(type) {
|
||||||
|
case *ledgerv1.PostCreditRequest:
|
||||||
|
idempotencyKey = r.GetIdempotencyKey()
|
||||||
|
orgRef = r.GetOrganizationRef()
|
||||||
|
description = r.GetDescription()
|
||||||
|
metadata = r.GetMetadata()
|
||||||
|
charges = r.GetCharges()
|
||||||
|
eventTime = r.GetEventTime()
|
||||||
|
contraRef = r.GetContraLedgerAccountRef()
|
||||||
|
case *ledgerv1.PostDebitRequest:
|
||||||
|
idempotencyKey = r.GetIdempotencyKey()
|
||||||
|
orgRef = r.GetOrganizationRef()
|
||||||
|
description = r.GetDescription()
|
||||||
|
metadata = r.GetMetadata()
|
||||||
|
charges = r.GetCharges()
|
||||||
|
eventTime = r.GetEventTime()
|
||||||
|
contraRef = r.GetContraLedgerAccountRef()
|
||||||
|
case *ledgerv1.TransferRequest:
|
||||||
|
idempotencyKey = r.GetIdempotencyKey()
|
||||||
|
orgRef = r.GetOrganizationRef()
|
||||||
|
description = r.GetDescription()
|
||||||
|
metadata = r.GetMetadata()
|
||||||
|
charges = r.GetCharges()
|
||||||
|
eventTime = r.GetEventTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
params := ledgerOperationParams(orgRef, description, metadata, charges, eventTime)
|
||||||
|
if contraRef != "" {
|
||||||
|
params["contra_ledger_account_ref"] = strings.TrimSpace(contraRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
op := &connectorv1.Operation{
|
||||||
|
Type: opType,
|
||||||
|
IdempotencyKey: strings.TrimSpace(idempotencyKey),
|
||||||
|
Money: money,
|
||||||
|
Params: structFromMap(params),
|
||||||
|
}
|
||||||
|
if fromRef != "" {
|
||||||
|
op.From = accountParty(fromRef)
|
||||||
|
}
|
||||||
|
if toRef != "" {
|
||||||
|
op.To = accountParty(toRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: op})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||||
|
return nil, connectorError(resp.GetReceipt().GetError())
|
||||||
|
}
|
||||||
|
return &ledgerv1.PostResponse{JournalEntryRef: resp.GetReceipt().GetOperationId(), EntryType: entryTypeFromOperation(opType)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerOperationParams(orgRef, description string, metadata map[string]string, charges []*ledgerv1.PostingLine, eventTime *timestamppb.Timestamp) map[string]interface{} {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"organization_ref": strings.TrimSpace(orgRef),
|
||||||
|
"description": strings.TrimSpace(description),
|
||||||
|
}
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
params["metadata"] = mapStringToInterface(metadata)
|
||||||
|
}
|
||||||
|
if len(charges) > 0 {
|
||||||
|
params["charges"] = chargesToInterface(charges)
|
||||||
|
}
|
||||||
|
if eventTime != nil {
|
||||||
|
params["event_time"] = eventTime.AsTime().UTC().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountParty(accountRef string) *connectorv1.OperationParty {
|
||||||
|
if strings.TrimSpace(accountRef) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(accountRef)}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryTypeFromOperation(opType connectorv1.OperationType) ledgerv1.EntryType {
|
||||||
|
switch opType {
|
||||||
|
case connectorv1.OperationType_CREDIT:
|
||||||
|
return ledgerv1.EntryType_ENTRY_CREDIT
|
||||||
|
case connectorv1.OperationType_DEBIT:
|
||||||
|
return ledgerv1.EntryType_ENTRY_DEBIT
|
||||||
|
case connectorv1.OperationType_TRANSFER:
|
||||||
|
return ledgerv1.EntryType_ENTRY_TRANSFER
|
||||||
|
case connectorv1.OperationType_FX:
|
||||||
|
return ledgerv1.EntryType_ENTRY_FX
|
||||||
|
default:
|
||||||
|
return ledgerv1.EntryType_ENTRY_TYPE_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAccount {
|
||||||
|
if account == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
details := map[string]interface{}{}
|
||||||
|
if account.GetProviderDetails() != nil {
|
||||||
|
details = account.GetProviderDetails().AsMap()
|
||||||
|
}
|
||||||
|
accountType := ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
|
||||||
|
if v := strings.TrimSpace(fmt.Sprint(details["account_type"])); v != "" {
|
||||||
|
accountType = parseAccountType(v)
|
||||||
|
}
|
||||||
|
status := ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
|
||||||
|
if v := strings.TrimSpace(fmt.Sprint(details["status"])); v != "" {
|
||||||
|
status = parseAccountStatus(v)
|
||||||
|
}
|
||||||
|
allowNegative := false
|
||||||
|
if v, ok := details["allow_negative"].(bool); ok {
|
||||||
|
allowNegative = v
|
||||||
|
}
|
||||||
|
isSettlement := false
|
||||||
|
if v, ok := details["is_settlement"].(bool); ok {
|
||||||
|
isSettlement = v
|
||||||
|
}
|
||||||
|
accountCode := strings.TrimSpace(fmt.Sprint(details["account_code"]))
|
||||||
|
accountID := ""
|
||||||
|
if ref := account.GetRef(); ref != nil {
|
||||||
|
accountID = strings.TrimSpace(ref.GetAccountId())
|
||||||
|
}
|
||||||
|
return &ledgerv1.LedgerAccount{
|
||||||
|
LedgerAccountRef: accountID,
|
||||||
|
OrganizationRef: strings.TrimSpace(account.GetOwnerRef()),
|
||||||
|
AccountCode: accountCode,
|
||||||
|
AccountType: accountType,
|
||||||
|
Currency: strings.TrimSpace(account.GetAsset()),
|
||||||
|
Status: status,
|
||||||
|
AllowNegative: allowNegative,
|
||||||
|
IsSettlement: isSettlement,
|
||||||
|
CreatedAt: account.GetCreatedAt(),
|
||||||
|
UpdatedAt: account.GetUpdatedAt(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAccountType(value string) ledgerv1.AccountType {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET
|
||||||
|
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY
|
||||||
|
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE
|
||||||
|
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAccountStatus(value string) ledgerv1.AccountStatus {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE
|
||||||
|
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func journalEntryFromOperation(op *connectorv1.Operation) *ledgerv1.JournalEntryResponse {
|
||||||
|
if op == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entry := &ledgerv1.JournalEntryResponse{
|
||||||
|
EntryRef: strings.TrimSpace(op.GetOperationId()),
|
||||||
|
EntryType: entryTypeFromOperation(op.GetType()),
|
||||||
|
Description: operationDescription(op),
|
||||||
|
EventTime: op.GetCreatedAt(),
|
||||||
|
Lines: postingLinesFromOperation(op),
|
||||||
|
LedgerAccountRefs: ledgerAccountRefsFromOperation(op),
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationDescription(op *connectorv1.Operation) string {
|
||||||
|
if op == nil || op.GetParams() == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if value, ok := op.GetParams().AsMap()["description"]; ok {
|
||||||
|
return strings.TrimSpace(fmt.Sprint(value))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func postingLinesFromOperation(op *connectorv1.Operation) []*ledgerv1.PostingLine {
|
||||||
|
if op == nil || op.GetMoney() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lines := []*ledgerv1.PostingLine{}
|
||||||
|
if from := op.GetFrom(); from != nil && from.GetAccount() != nil {
|
||||||
|
lines = append(lines, &ledgerv1.PostingLine{LedgerAccountRef: strings.TrimSpace(from.GetAccount().GetAccountId()), Money: op.GetMoney(), LineType: ledgerv1.LineType_LINE_MAIN})
|
||||||
|
}
|
||||||
|
if to := op.GetTo(); to != nil && to.GetAccount() != nil {
|
||||||
|
lines = append(lines, &ledgerv1.PostingLine{LedgerAccountRef: strings.TrimSpace(to.GetAccount().GetAccountId()), Money: op.GetMoney(), LineType: ledgerv1.LineType_LINE_MAIN})
|
||||||
|
}
|
||||||
|
if len(lines) == 0 {
|
||||||
|
lines = append(lines, &ledgerv1.PostingLine{Money: op.GetMoney(), LineType: ledgerv1.LineType_LINE_MAIN})
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerAccountRefsFromOperation(op *connectorv1.Operation) []string {
|
||||||
|
refs := []string{}
|
||||||
|
if op == nil {
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
if from := op.GetFrom(); from != nil && from.GetAccount() != nil {
|
||||||
|
refs = append(refs, strings.TrimSpace(from.GetAccount().GetAccountId()))
|
||||||
|
}
|
||||||
|
if to := op.GetTo(); to != nil && to.GetAccount() != nil {
|
||||||
|
refs = append(refs, strings.TrimSpace(to.GetAccount().GetAccountId()))
|
||||||
|
}
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageFromStatement(req *ledgerv1.GetStatementRequest) *paginationv1.CursorPageRequest {
|
||||||
|
if req == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &paginationv1.CursorPageRequest{
|
||||||
|
Cursor: strings.TrimSpace(req.GetCursor()),
|
||||||
|
Limit: req.GetLimit(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chargesToInterface(charges []*ledgerv1.PostingLine) []interface{} {
|
||||||
|
if len(charges) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]interface{}, 0, len(charges))
|
||||||
|
for _, line := range charges {
|
||||||
|
if line == nil || line.GetMoney() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"ledger_account_ref": strings.TrimSpace(line.GetLedgerAccountRef()),
|
||||||
|
"amount": strings.TrimSpace(line.GetMoney().GetAmount()),
|
||||||
|
"currency": strings.TrimSpace(line.GetMoney().GetCurrency()),
|
||||||
|
"line_type": line.GetLineType().String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (c *ledgerClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
func (c *ledgerClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
|||||||
622
api/ledger/internal/service/ledger/connector.go
Normal file
622
api/ledger/internal/service/ledger/connector.go
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
package ledger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/ledger/internal/appversion"
|
||||||
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ledgerConnectorID = "ledger"
|
||||||
|
|
||||||
|
type connectorAdapter struct {
|
||||||
|
svc *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConnectorAdapter(svc *Service) *connectorAdapter {
|
||||||
|
return &connectorAdapter{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorAdapter) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||||
|
return &connectorv1.GetCapabilitiesResponse{
|
||||||
|
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||||
|
ConnectorType: ledgerConnectorID,
|
||||||
|
Version: appversion.Create().Short(),
|
||||||
|
SupportedAccountKinds: []connectorv1.AccountKind{connectorv1.AccountKind_LEDGER_ACCOUNT},
|
||||||
|
SupportedOperationTypes: []connectorv1.OperationType{
|
||||||
|
connectorv1.OperationType_CREDIT,
|
||||||
|
connectorv1.OperationType_DEBIT,
|
||||||
|
connectorv1.OperationType_TRANSFER,
|
||||||
|
connectorv1.OperationType_FX,
|
||||||
|
},
|
||||||
|
OpenAccountParams: ledgerOpenAccountParams(),
|
||||||
|
OperationParams: ledgerOperationParams(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
|
||||||
|
if req == nil {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil, "")}, nil
|
||||||
|
}
|
||||||
|
if req.GetKind() != connectorv1.AccountKind_LEDGER_ACCOUNT {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil, "")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := params.New(req.GetParams())
|
||||||
|
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||||||
|
accountCode := strings.TrimSpace(reader.String("account_code"))
|
||||||
|
if orgRef == "" || accountCode == "" {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref and account_code are required", nil, "")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
accountType, err := parseLedgerAccountType(reader, "account_type")
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil, "")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currency := strings.TrimSpace(req.GetAsset())
|
||||||
|
if currency == "" {
|
||||||
|
currency = strings.TrimSpace(reader.String("currency"))
|
||||||
|
}
|
||||||
|
if currency == "" {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: asset is required", nil, "")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status := parseLedgerAccountStatus(reader, "status")
|
||||||
|
metadata := mergeMetadata(reader.StringMap("metadata"), req.GetLabel(), req.GetOwnerRef(), req.GetCorrelationId(), req.GetParentIntentId())
|
||||||
|
|
||||||
|
resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
||||||
|
OrganizationRef: orgRef,
|
||||||
|
AccountCode: accountCode,
|
||||||
|
AccountType: accountType,
|
||||||
|
Currency: currency,
|
||||||
|
Status: status,
|
||||||
|
AllowNegative: reader.Bool("allow_negative"),
|
||||||
|
IsSettlement: reader.Bool("is_settlement"),
|
||||||
|
Metadata: metadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &connectorv1.OpenAccountResponse{
|
||||||
|
Account: ledgerAccountToConnector(resp.GetAccount()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorAdapter) GetAccount(ctx context.Context, req *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
|
||||||
|
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("get_account: account_ref.account_id is required")
|
||||||
|
}
|
||||||
|
accountRef, err := parseObjectID(strings.TrimSpace(req.GetAccountRef().GetAccountId()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.svc.storage == nil || c.svc.storage.Accounts() == nil {
|
||||||
|
return nil, merrors.Internal("get_account: storage unavailable")
|
||||||
|
}
|
||||||
|
account, err := c.svc.storage.Accounts().Get(ctx, accountRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &connectorv1.GetAccountResponse{
|
||||||
|
Account: ledgerAccountToConnector(toProtoAccount(account)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorAdapter) ListAccounts(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
|
||||||
|
if req == nil || strings.TrimSpace(req.GetOwnerRef()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("list_accounts: owner_ref is required")
|
||||||
|
}
|
||||||
|
resp, err := c.svc.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{OrganizationRef: strings.TrimSpace(req.GetOwnerRef())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
accounts := make([]*connectorv1.Account, 0, len(resp.GetAccounts()))
|
||||||
|
for _, account := range resp.GetAccounts() {
|
||||||
|
accounts = append(accounts, ledgerAccountToConnector(account))
|
||||||
|
}
|
||||||
|
return &connectorv1.ListAccountsResponse{Accounts: accounts}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorAdapter) GetBalance(ctx context.Context, req *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
|
||||||
|
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("get_balance: account_ref.account_id is required")
|
||||||
|
}
|
||||||
|
resp, err := c.svc.GetBalance(ctx, &ledgerv1.GetBalanceRequest{
|
||||||
|
LedgerAccountRef: strings.TrimSpace(req.GetAccountRef().GetAccountId()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &connectorv1.GetBalanceResponse{
|
||||||
|
Balance: &connectorv1.Balance{
|
||||||
|
AccountRef: req.GetAccountRef(),
|
||||||
|
Available: resp.GetBalance(),
|
||||||
|
CalculatedAt: resp.GetLastUpdated(),
|
||||||
|
PendingInbound: nil,
|
||||||
|
PendingOutbound: nil,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
||||||
|
if req == nil || req.GetOperation() == nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil
|
||||||
|
}
|
||||||
|
op := req.GetOperation()
|
||||||
|
if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
reader := params.New(op.GetParams())
|
||||||
|
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||||||
|
if orgRef == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: organization_ref is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := mergeMetadata(reader.StringMap("metadata"), "", "", op.GetCorrelationId(), op.GetParentIntentId())
|
||||||
|
description := strings.TrimSpace(reader.String("description"))
|
||||||
|
eventTime := parseEventTime(reader)
|
||||||
|
charges, err := parseLedgerCharges(reader)
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch op.GetType() {
|
||||||
|
case connectorv1.OperationType_CREDIT:
|
||||||
|
accountID := operationAccountID(op.GetTo())
|
||||||
|
if accountID == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "credit: to.account is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
resp, err := c.svc.PostCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
||||||
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
OrganizationRef: orgRef,
|
||||||
|
LedgerAccountRef: accountID,
|
||||||
|
Money: op.GetMoney(),
|
||||||
|
Description: description,
|
||||||
|
Charges: charges,
|
||||||
|
Metadata: metadata,
|
||||||
|
EventTime: eventTime,
|
||||||
|
ContraLedgerAccountRef: strings.TrimSpace(reader.String("contra_ledger_account_ref")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, accountID)}}, nil
|
||||||
|
}
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_CONFIRMED)}, nil
|
||||||
|
case connectorv1.OperationType_DEBIT:
|
||||||
|
accountID := operationAccountID(op.GetFrom())
|
||||||
|
if accountID == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "debit: from.account is required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
resp, err := c.svc.PostDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
||||||
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
OrganizationRef: orgRef,
|
||||||
|
LedgerAccountRef: accountID,
|
||||||
|
Money: op.GetMoney(),
|
||||||
|
Description: description,
|
||||||
|
Charges: charges,
|
||||||
|
Metadata: metadata,
|
||||||
|
EventTime: eventTime,
|
||||||
|
ContraLedgerAccountRef: strings.TrimSpace(reader.String("contra_ledger_account_ref")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, accountID)}}, nil
|
||||||
|
}
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_CONFIRMED)}, nil
|
||||||
|
case connectorv1.OperationType_TRANSFER:
|
||||||
|
fromID := operationAccountID(op.GetFrom())
|
||||||
|
toID := operationAccountID(op.GetTo())
|
||||||
|
if fromID == "" || toID == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account and to.account are required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
resp, err := c.svc.TransferInternal(ctx, &ledgerv1.TransferRequest{
|
||||||
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
OrganizationRef: orgRef,
|
||||||
|
FromLedgerAccountRef: fromID,
|
||||||
|
ToLedgerAccountRef: toID,
|
||||||
|
Money: op.GetMoney(),
|
||||||
|
Description: description,
|
||||||
|
Charges: charges,
|
||||||
|
Metadata: metadata,
|
||||||
|
EventTime: eventTime,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_CONFIRMED)}, nil
|
||||||
|
case connectorv1.OperationType_FX:
|
||||||
|
fromID := operationAccountID(op.GetFrom())
|
||||||
|
toID := operationAccountID(op.GetTo())
|
||||||
|
if fromID == "" || toID == "" {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "fx: from.account and to.account are required", op, "")}}, nil
|
||||||
|
}
|
||||||
|
toMoney, err := parseMoneyFromMap(reader.Map("to_money"))
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
resp, err := c.svc.ApplyFXWithCharges(ctx, &ledgerv1.FXRequest{
|
||||||
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
OrganizationRef: orgRef,
|
||||||
|
FromLedgerAccountRef: fromID,
|
||||||
|
ToLedgerAccountRef: toID,
|
||||||
|
FromMoney: op.GetMoney(),
|
||||||
|
ToMoney: toMoney,
|
||||||
|
Rate: strings.TrimSpace(reader.String("rate")),
|
||||||
|
Description: description,
|
||||||
|
Charges: charges,
|
||||||
|
Metadata: metadata,
|
||||||
|
EventTime: eventTime,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
|
}
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_CONFIRMED)}, nil
|
||||||
|
default:
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorAdapter) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
|
||||||
|
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
|
||||||
|
}
|
||||||
|
entry, err := c.svc.GetJournalEntry(ctx, &ledgerv1.GetEntryRequest{EntryRef: strings.TrimSpace(req.GetOperationId())})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &connectorv1.GetOperationResponse{Operation: ledgerEntryToOperation(entry)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorAdapter) ListOperations(ctx context.Context, req *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
|
||||||
|
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("list_operations: account_ref.account_id is required")
|
||||||
|
}
|
||||||
|
resp, err := c.svc.GetStatement(ctx, &ledgerv1.GetStatementRequest{
|
||||||
|
LedgerAccountRef: strings.TrimSpace(req.GetAccountRef().GetAccountId()),
|
||||||
|
Cursor: "",
|
||||||
|
Limit: 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ops := make([]*connectorv1.Operation, 0, len(resp.GetEntries()))
|
||||||
|
for _, entry := range resp.GetEntries() {
|
||||||
|
ops = append(ops, ledgerEntryToOperation(entry))
|
||||||
|
}
|
||||||
|
return &connectorv1.ListOperationsResponse{Operations: ops}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerOpenAccountParams() []*connectorv1.ParamSpec {
|
||||||
|
return []*connectorv1.ParamSpec{
|
||||||
|
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference for the ledger account."},
|
||||||
|
{Key: "account_code", Type: connectorv1.ParamType_STRING, Required: true, Description: "Ledger account code."},
|
||||||
|
{Key: "account_type", Type: connectorv1.ParamType_STRING, Required: true, Description: "ASSET | LIABILITY | REVENUE | EXPENSE."},
|
||||||
|
{Key: "status", Type: connectorv1.ParamType_STRING, Required: false, Description: "ACTIVE | FROZEN."},
|
||||||
|
{Key: "allow_negative", Type: connectorv1.ParamType_BOOL, Required: false, Description: "Allow negative balance."},
|
||||||
|
{Key: "is_settlement", Type: connectorv1.ParamType_BOOL, Required: false, Description: "Mark account as settlement."},
|
||||||
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Additional metadata map."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerOperationParams() []*connectorv1.OperationParamSpec {
|
||||||
|
common := []*connectorv1.ParamSpec{
|
||||||
|
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference."},
|
||||||
|
{Key: "description", Type: connectorv1.ParamType_STRING, Required: false, Description: "Ledger entry description."},
|
||||||
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Entry metadata map."},
|
||||||
|
{Key: "charges", Type: connectorv1.ParamType_JSON, Required: false, Description: "Posting line charges."},
|
||||||
|
{Key: "event_time", Type: connectorv1.ParamType_STRING, Required: false, Description: "RFC3339 timestamp."},
|
||||||
|
}
|
||||||
|
return []*connectorv1.OperationParamSpec{
|
||||||
|
{OperationType: connectorv1.OperationType_CREDIT, Params: append(common, &connectorv1.ParamSpec{Key: "contra_ledger_account_ref", Type: connectorv1.ParamType_STRING, Required: false})},
|
||||||
|
{OperationType: connectorv1.OperationType_DEBIT, Params: append(common, &connectorv1.ParamSpec{Key: "contra_ledger_account_ref", Type: connectorv1.ParamType_STRING, Required: false})},
|
||||||
|
{OperationType: connectorv1.OperationType_TRANSFER, Params: common},
|
||||||
|
{OperationType: connectorv1.OperationType_FX, Params: append(common,
|
||||||
|
&connectorv1.ParamSpec{Key: "to_money", Type: connectorv1.ParamType_JSON, Required: true, Description: "Target amount {amount,currency}."},
|
||||||
|
&connectorv1.ParamSpec{Key: "rate", Type: connectorv1.ParamType_STRING, Required: false, Description: "FX rate snapshot."},
|
||||||
|
)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerAccountToConnector(account *ledgerv1.LedgerAccount) *connectorv1.Account {
|
||||||
|
if account == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
details, _ := structpb.NewStruct(map[string]interface{}{
|
||||||
|
"account_code": account.GetAccountCode(),
|
||||||
|
"account_type": account.GetAccountType().String(),
|
||||||
|
"status": account.GetStatus().String(),
|
||||||
|
"allow_negative": account.GetAllowNegative(),
|
||||||
|
"is_settlement": account.GetIsSettlement(),
|
||||||
|
})
|
||||||
|
return &connectorv1.Account{
|
||||||
|
Ref: &connectorv1.AccountRef{
|
||||||
|
ConnectorId: ledgerConnectorID,
|
||||||
|
AccountId: strings.TrimSpace(account.GetLedgerAccountRef()),
|
||||||
|
},
|
||||||
|
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
|
||||||
|
Asset: strings.TrimSpace(account.GetCurrency()),
|
||||||
|
State: ledgerAccountState(account.GetStatus()),
|
||||||
|
Label: strings.TrimSpace(account.GetAccountCode()),
|
||||||
|
OwnerRef: strings.TrimSpace(account.GetOrganizationRef()),
|
||||||
|
ProviderDetails: details,
|
||||||
|
CreatedAt: account.GetCreatedAt(),
|
||||||
|
UpdatedAt: account.GetUpdatedAt(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerAccountState(status ledgerv1.AccountStatus) connectorv1.AccountState {
|
||||||
|
switch status {
|
||||||
|
case ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE:
|
||||||
|
return connectorv1.AccountState_ACCOUNT_ACTIVE
|
||||||
|
case ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN:
|
||||||
|
return connectorv1.AccountState_ACCOUNT_SUSPENDED
|
||||||
|
default:
|
||||||
|
return connectorv1.AccountState_ACCOUNT_STATE_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerReceipt(ref string, status connectorv1.OperationStatus) *connectorv1.OperationReceipt {
|
||||||
|
return &connectorv1.OperationReceipt{
|
||||||
|
OperationId: strings.TrimSpace(ref),
|
||||||
|
Status: status,
|
||||||
|
ProviderRef: strings.TrimSpace(ref),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerEntryToOperation(entry *ledgerv1.JournalEntryResponse) *connectorv1.Operation {
|
||||||
|
if entry == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
op := &connectorv1.Operation{
|
||||||
|
OperationId: strings.TrimSpace(entry.GetEntryRef()),
|
||||||
|
Type: ledgerEntryType(entry.GetEntryType()),
|
||||||
|
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||||
|
CreatedAt: entry.GetEventTime(),
|
||||||
|
UpdatedAt: entry.GetEventTime(),
|
||||||
|
}
|
||||||
|
mainLines := ledgerMainLines(entry.GetLines())
|
||||||
|
if len(mainLines) > 0 {
|
||||||
|
op.Money = mainLines[0].GetMoney()
|
||||||
|
}
|
||||||
|
switch op.Type {
|
||||||
|
case connectorv1.OperationType_CREDIT:
|
||||||
|
if len(mainLines) > 0 {
|
||||||
|
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
|
||||||
|
}
|
||||||
|
case connectorv1.OperationType_DEBIT:
|
||||||
|
if len(mainLines) > 0 {
|
||||||
|
op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
|
||||||
|
}
|
||||||
|
case connectorv1.OperationType_TRANSFER, connectorv1.OperationType_FX:
|
||||||
|
if len(mainLines) > 0 {
|
||||||
|
op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
|
||||||
|
}
|
||||||
|
if len(mainLines) > 1 {
|
||||||
|
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[1].GetLedgerAccountRef()}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerMainLines(lines []*ledgerv1.PostingLine) []*ledgerv1.PostingLine {
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]*ledgerv1.PostingLine, 0, len(lines))
|
||||||
|
for _, line := range lines {
|
||||||
|
if line == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line.GetLineType() == ledgerv1.LineType_LINE_MAIN {
|
||||||
|
result = append(result, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledgerEntryType(entryType ledgerv1.EntryType) connectorv1.OperationType {
|
||||||
|
switch entryType {
|
||||||
|
case ledgerv1.EntryType_ENTRY_CREDIT:
|
||||||
|
return connectorv1.OperationType_CREDIT
|
||||||
|
case ledgerv1.EntryType_ENTRY_DEBIT:
|
||||||
|
return connectorv1.OperationType_DEBIT
|
||||||
|
case ledgerv1.EntryType_ENTRY_TRANSFER:
|
||||||
|
return connectorv1.OperationType_TRANSFER
|
||||||
|
case ledgerv1.EntryType_ENTRY_FX:
|
||||||
|
return connectorv1.OperationType_FX
|
||||||
|
default:
|
||||||
|
return connectorv1.OperationType_OPERATION_TYPE_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationAccountID(party *connectorv1.OperationParty) string {
|
||||||
|
if party == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if account := party.GetAccount(); account != nil {
|
||||||
|
return strings.TrimSpace(account.GetAccountId())
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountType, error) {
|
||||||
|
value, ok := reader.Value(key)
|
||||||
|
if !ok {
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: account_type is required")
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
return parseLedgerAccountTypeString(v)
|
||||||
|
case float64:
|
||||||
|
return ledgerv1.AccountType(int32(v)), nil
|
||||||
|
case int:
|
||||||
|
return ledgerv1.AccountType(v), nil
|
||||||
|
case int64:
|
||||||
|
return ledgerv1.AccountType(v), nil
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: account_type is required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, nil
|
||||||
|
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY, nil
|
||||||
|
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE, nil
|
||||||
|
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: invalid account_type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLedgerAccountStatus(reader params.Reader, key string) ledgerv1.AccountStatus {
|
||||||
|
value := strings.ToUpper(strings.TrimSpace(reader.String(key)))
|
||||||
|
switch value {
|
||||||
|
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE
|
||||||
|
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEventTime(reader params.Reader) *timestamppb.Timestamp {
|
||||||
|
raw := strings.TrimSpace(reader.String("event_time"))
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(time.RFC3339Nano, raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return timestamppb.New(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLedgerCharges(reader params.Reader) ([]*ledgerv1.PostingLine, error) {
|
||||||
|
items := reader.List("charges")
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
result := make([]*ledgerv1.PostingLine, 0, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
raw, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("charges[%d]: invalid charge entry", i)
|
||||||
|
}
|
||||||
|
accountRef := strings.TrimSpace(fmt.Sprint(raw["ledger_account_ref"]))
|
||||||
|
if accountRef == "" {
|
||||||
|
return nil, fmt.Errorf("charges[%d]: ledger_account_ref is required", i)
|
||||||
|
}
|
||||||
|
money, err := parseMoneyFromMap(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("charges[%d]: %w", i, err)
|
||||||
|
}
|
||||||
|
lineType := parseLedgerLineType(fmt.Sprint(raw["line_type"]))
|
||||||
|
result = append(result, &ledgerv1.PostingLine{
|
||||||
|
LedgerAccountRef: accountRef,
|
||||||
|
Money: money,
|
||||||
|
LineType: lineType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLedgerLineType(value string) ledgerv1.LineType {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "LINE_TYPE_FEE", "FEE":
|
||||||
|
return ledgerv1.LineType_LINE_FEE
|
||||||
|
case "LINE_TYPE_SPREAD", "SPREAD":
|
||||||
|
return ledgerv1.LineType_LINE_SPREAD
|
||||||
|
case "LINE_TYPE_REVERSAL", "REVERSAL":
|
||||||
|
return ledgerv1.LineType_LINE_REVERSAL
|
||||||
|
default:
|
||||||
|
return ledgerv1.LineType_LINE_FEE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
|
||||||
|
if raw == nil {
|
||||||
|
return nil, fmt.Errorf("money is required")
|
||||||
|
}
|
||||||
|
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
||||||
|
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
||||||
|
if amount == "" || currency == "" {
|
||||||
|
return nil, fmt.Errorf("money is required")
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Amount: amount,
|
||||||
|
Currency: currency,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeMetadata(base map[string]string, label, ownerRef, correlationID, parentIntentID string) map[string]string {
|
||||||
|
metadata := map[string]string{}
|
||||||
|
for k, v := range base {
|
||||||
|
metadata[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
if label != "" {
|
||||||
|
if _, ok := metadata["label"]; !ok {
|
||||||
|
metadata["label"] = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ownerRef != "" {
|
||||||
|
if _, ok := metadata["owner_ref"]; !ok {
|
||||||
|
metadata["owner_ref"] = ownerRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if correlationID != "" {
|
||||||
|
metadata["correlation_id"] = correlationID
|
||||||
|
}
|
||||||
|
if parentIntentID != "" {
|
||||||
|
metadata["parent_intent_id"] = parentIntentID
|
||||||
|
}
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
||||||
|
err := &connectorv1.ConnectorError{
|
||||||
|
Code: code,
|
||||||
|
Message: strings.TrimSpace(message),
|
||||||
|
AccountId: strings.TrimSpace(accountID),
|
||||||
|
}
|
||||||
|
if op != nil {
|
||||||
|
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
||||||
|
err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
|
||||||
|
err.OperationId = strings.TrimSpace(op.GetOperationId())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapErrorCode(err error) connectorv1.ErrorCode {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, merrors.ErrInvalidArg):
|
||||||
|
return connectorv1.ErrorCode_INVALID_PARAMS
|
||||||
|
case errors.Is(err, merrors.ErrNoData):
|
||||||
|
return connectorv1.ErrorCode_NOT_FOUND
|
||||||
|
case errors.Is(err, merrors.ErrNotImplemented):
|
||||||
|
return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
|
||||||
|
case errors.Is(err, merrors.ErrInternal):
|
||||||
|
return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
|
||||||
|
default:
|
||||||
|
return connectorv1.ErrorCode_PROVIDER_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
pmessaging "github.com/tech/sendico/pkg/messaging"
|
pmessaging "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +50,6 @@ type Service struct {
|
|||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
publisher *outboxPublisher
|
publisher *outboxPublisher
|
||||||
}
|
}
|
||||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type feesDependency struct {
|
type feesDependency struct {
|
||||||
@@ -83,7 +82,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.
|
|||||||
|
|
||||||
func (s *Service) Register(router routers.GRPC) error {
|
func (s *Service) Register(router routers.GRPC) error {
|
||||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
connectorv1.RegisterConnectorServiceServer(reg, newConnectorAdapter(s))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
138
api/pkg/connector/params/params.go
Normal file
138
api/pkg/connector/params/params.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package params
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reader provides typed helpers around a Struct param payload.
|
||||||
|
type Reader struct {
|
||||||
|
raw map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New builds a Reader for the provided struct, tolerating nil inputs.
|
||||||
|
func New(params *structpb.Struct) Reader {
|
||||||
|
if params == nil {
|
||||||
|
return Reader{}
|
||||||
|
}
|
||||||
|
return Reader{raw: params.AsMap()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the raw value and whether it was present.
|
||||||
|
func (r Reader) Value(key string) (interface{}, bool) {
|
||||||
|
if r.raw == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
value, ok := r.raw[key]
|
||||||
|
return value, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string value for a key, or "" if missing/not a string.
|
||||||
|
func (r Reader) String(key string) string {
|
||||||
|
value, ok := r.Value(key)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
return v
|
||||||
|
case fmt.Stringer:
|
||||||
|
return v.String()
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns the bool value for a key, or false when missing/not a bool.
|
||||||
|
func (r Reader) Bool(key string) bool {
|
||||||
|
value, ok := r.Value(key)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case bool:
|
||||||
|
return v
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 returns the int64 value for a key with a presence flag.
|
||||||
|
func (r Reader) Int64(key string) (int64, bool) {
|
||||||
|
value, ok := r.Value(key)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int64:
|
||||||
|
return v, true
|
||||||
|
case int32:
|
||||||
|
return int64(v), true
|
||||||
|
case int:
|
||||||
|
return int64(v), true
|
||||||
|
case float64:
|
||||||
|
return int64(v), true
|
||||||
|
case float32:
|
||||||
|
return int64(v), true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 returns the float64 value for a key with a presence flag.
|
||||||
|
func (r Reader) Float64(key string) (float64, bool) {
|
||||||
|
value, ok := r.Value(key)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return v, true
|
||||||
|
case float32:
|
||||||
|
return float64(v), true
|
||||||
|
case int:
|
||||||
|
return float64(v), true
|
||||||
|
case int64:
|
||||||
|
return float64(v), true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map returns a nested object value as a map.
|
||||||
|
func (r Reader) Map(key string) map[string]interface{} {
|
||||||
|
value, ok := r.Value(key)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if m, ok := value.(map[string]interface{}); ok {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a list value as a slice.
|
||||||
|
func (r Reader) List(key string) []interface{} {
|
||||||
|
value, ok := r.Value(key)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if list, ok := value.([]interface{}); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringMap converts a nested map into a string map.
|
||||||
|
func (r Reader) StringMap(key string) map[string]string {
|
||||||
|
raw := r.Map(key)
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]string, len(raw))
|
||||||
|
for k, v := range raw {
|
||||||
|
out[k] = fmt.Sprint(v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
256
api/proto/connector/v1/connector.proto
Normal file
256
api/proto/connector/v1/connector.proto
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package connector.v1;
|
||||||
|
|
||||||
|
option go_package = "github.com/tech/sendico/pkg/proto/connector/v1;connectorv1";
|
||||||
|
|
||||||
|
import "google/protobuf/struct.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "common/money/v1/money.proto";
|
||||||
|
import "common/pagination/v1/cursor.proto";
|
||||||
|
|
||||||
|
// ConnectorService exposes capability-driven account and operation primitives.
|
||||||
|
service ConnectorService {
|
||||||
|
rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse);
|
||||||
|
|
||||||
|
rpc OpenAccount(OpenAccountRequest) returns (OpenAccountResponse);
|
||||||
|
rpc GetAccount(GetAccountRequest) returns (GetAccountResponse);
|
||||||
|
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse);
|
||||||
|
rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse);
|
||||||
|
|
||||||
|
rpc SubmitOperation(SubmitOperationRequest) returns (SubmitOperationResponse);
|
||||||
|
rpc GetOperation(GetOperationRequest) returns (GetOperationResponse);
|
||||||
|
rpc ListOperations(ListOperationsRequest) returns (ListOperationsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AccountKind {
|
||||||
|
ACCOUNT_KIND_UNSPECIFIED = 0;
|
||||||
|
LEDGER_ACCOUNT = 1;
|
||||||
|
CHAIN_MANAGED_WALLET = 2;
|
||||||
|
EXTERNAL_REF = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AccountState {
|
||||||
|
ACCOUNT_STATE_UNSPECIFIED = 0;
|
||||||
|
ACCOUNT_ACTIVE = 1;
|
||||||
|
ACCOUNT_SUSPENDED = 2;
|
||||||
|
ACCOUNT_CLOSED = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OperationType {
|
||||||
|
OPERATION_TYPE_UNSPECIFIED = 0;
|
||||||
|
CREDIT = 1;
|
||||||
|
DEBIT = 2;
|
||||||
|
TRANSFER = 3;
|
||||||
|
PAYOUT = 4;
|
||||||
|
FEE_ESTIMATE = 5;
|
||||||
|
FX = 6;
|
||||||
|
GAS_TOPUP = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OperationStatus {
|
||||||
|
OPERATION_STATUS_UNSPECIFIED = 0;
|
||||||
|
SUBMITTED = 1;
|
||||||
|
PENDING = 2;
|
||||||
|
CONFIRMED = 3;
|
||||||
|
FAILED = 4;
|
||||||
|
CANCELED = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ParamType {
|
||||||
|
PARAM_TYPE_UNSPECIFIED = 0;
|
||||||
|
STRING = 1;
|
||||||
|
INT = 2;
|
||||||
|
BOOL = 3;
|
||||||
|
DECIMAL_STRING = 4;
|
||||||
|
JSON = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ErrorCode {
|
||||||
|
ERROR_CODE_UNSPECIFIED = 0;
|
||||||
|
UNSUPPORTED_OPERATION = 1;
|
||||||
|
UNSUPPORTED_ACCOUNT_KIND = 2;
|
||||||
|
INVALID_PARAMS = 3;
|
||||||
|
INSUFFICIENT_FUNDS = 4;
|
||||||
|
NOT_FOUND = 5;
|
||||||
|
TEMPORARY_UNAVAILABLE = 6;
|
||||||
|
RATE_LIMITED = 7;
|
||||||
|
PROVIDER_ERROR = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ParamSpec {
|
||||||
|
string key = 1;
|
||||||
|
ParamType type = 2;
|
||||||
|
bool required = 3;
|
||||||
|
string description = 4;
|
||||||
|
repeated string allowed_values = 5;
|
||||||
|
google.protobuf.Struct example = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OperationParamSpec {
|
||||||
|
OperationType operation_type = 1;
|
||||||
|
repeated ParamSpec params = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ConnectorCapabilities {
|
||||||
|
string connector_type = 1;
|
||||||
|
string version = 2;
|
||||||
|
repeated AccountKind supported_account_kinds = 3;
|
||||||
|
repeated OperationType supported_operation_types = 4;
|
||||||
|
repeated string supported_assets = 5; // canonical asset string (USD, ETH, USDT-TRC20)
|
||||||
|
repeated string supported_networks = 6; // optional, connector-defined names
|
||||||
|
repeated ParamSpec open_account_params = 7;
|
||||||
|
repeated OperationParamSpec operation_params = 8;
|
||||||
|
map<string, string> metadata = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AccountRef {
|
||||||
|
string connector_id = 1;
|
||||||
|
string account_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExternalRef {
|
||||||
|
string external_ref = 1;
|
||||||
|
google.protobuf.Struct details = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OperationParty {
|
||||||
|
oneof ref {
|
||||||
|
AccountRef account = 1;
|
||||||
|
ExternalRef external = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Account {
|
||||||
|
AccountRef ref = 1;
|
||||||
|
AccountKind kind = 2;
|
||||||
|
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
||||||
|
AccountState state = 4;
|
||||||
|
string label = 5;
|
||||||
|
string owner_ref = 6;
|
||||||
|
google.protobuf.Struct provider_details = 7;
|
||||||
|
google.protobuf.Timestamp created_at = 8;
|
||||||
|
google.protobuf.Timestamp updated_at = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Balance {
|
||||||
|
AccountRef account_ref = 1;
|
||||||
|
common.money.v1.Money available = 2;
|
||||||
|
common.money.v1.Money pending_inbound = 3;
|
||||||
|
common.money.v1.Money pending_outbound = 4;
|
||||||
|
google.protobuf.Timestamp calculated_at = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ConnectorError {
|
||||||
|
ErrorCode code = 1;
|
||||||
|
string message = 2;
|
||||||
|
google.protobuf.Struct details = 3;
|
||||||
|
string correlation_id = 4;
|
||||||
|
string parent_intent_id = 5;
|
||||||
|
string operation_id = 6;
|
||||||
|
string account_id = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Operation {
|
||||||
|
string operation_id = 1;
|
||||||
|
OperationType type = 2;
|
||||||
|
OperationParty from = 3;
|
||||||
|
OperationParty to = 4;
|
||||||
|
common.money.v1.Money money = 5;
|
||||||
|
string idempotency_key = 6;
|
||||||
|
google.protobuf.Struct params = 7;
|
||||||
|
string correlation_id = 8;
|
||||||
|
string parent_intent_id = 9;
|
||||||
|
OperationStatus status = 10;
|
||||||
|
string provider_ref = 11;
|
||||||
|
google.protobuf.Timestamp created_at = 12;
|
||||||
|
google.protobuf.Timestamp updated_at = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OperationReceipt {
|
||||||
|
string operation_id = 1;
|
||||||
|
OperationStatus status = 2;
|
||||||
|
string provider_ref = 3;
|
||||||
|
ConnectorError error = 4;
|
||||||
|
google.protobuf.Struct result = 5; // connector-specific output payload
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCapabilitiesRequest {}
|
||||||
|
|
||||||
|
message GetCapabilitiesResponse {
|
||||||
|
ConnectorCapabilities capabilities = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OpenAccountRequest {
|
||||||
|
string idempotency_key = 1;
|
||||||
|
AccountKind kind = 2;
|
||||||
|
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
||||||
|
string label = 4;
|
||||||
|
string owner_ref = 5;
|
||||||
|
google.protobuf.Struct params = 6;
|
||||||
|
string correlation_id = 7;
|
||||||
|
string parent_intent_id = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OpenAccountResponse {
|
||||||
|
Account account = 1;
|
||||||
|
ConnectorError error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAccountRequest {
|
||||||
|
AccountRef account_ref = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAccountResponse {
|
||||||
|
Account account = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAccountsRequest {
|
||||||
|
string owner_ref = 1;
|
||||||
|
AccountKind kind = 2;
|
||||||
|
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
||||||
|
common.pagination.v1.CursorPageRequest page = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAccountsResponse {
|
||||||
|
repeated Account accounts = 1;
|
||||||
|
common.pagination.v1.CursorPageResponse page = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetBalanceRequest {
|
||||||
|
AccountRef account_ref = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetBalanceResponse {
|
||||||
|
Balance balance = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubmitOperationRequest {
|
||||||
|
Operation operation = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubmitOperationResponse {
|
||||||
|
OperationReceipt receipt = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetOperationRequest {
|
||||||
|
string operation_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetOperationResponse {
|
||||||
|
Operation operation = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListOperationsRequest {
|
||||||
|
AccountRef account_ref = 1;
|
||||||
|
OperationType type = 2;
|
||||||
|
OperationStatus status = 3;
|
||||||
|
string correlation_id = 4;
|
||||||
|
string parent_intent_id = 5;
|
||||||
|
common.pagination.v1.CursorPageRequest page = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListOperationsResponse {
|
||||||
|
repeated Operation operations = 1;
|
||||||
|
common.pagination.v1.CursorPageResponse page = 2;
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package gateway.unified.v1;
|
|
||||||
|
|
||||||
option go_package = "github.com/tech/sendico/pkg/proto/gateway/unified/v1;unifiedv1";
|
|
||||||
|
|
||||||
import "gateway/chain/v1/chain.proto";
|
|
||||||
import "gateway/mntx/v1/mntx.proto";
|
|
||||||
import "ledger/v1/ledger.proto";
|
|
||||||
|
|
||||||
// UnifiedGatewayService exposes gateway and ledger operations via a single interface.
|
|
||||||
service UnifiedGatewayService {
|
|
||||||
// Chain gateway operations.
|
|
||||||
rpc CreateManagedWallet(chain.gateway.v1.CreateManagedWalletRequest) returns (chain.gateway.v1.CreateManagedWalletResponse);
|
|
||||||
rpc GetManagedWallet(chain.gateway.v1.GetManagedWalletRequest) returns (chain.gateway.v1.GetManagedWalletResponse);
|
|
||||||
rpc ListManagedWallets(chain.gateway.v1.ListManagedWalletsRequest) returns (chain.gateway.v1.ListManagedWalletsResponse);
|
|
||||||
rpc GetWalletBalance(chain.gateway.v1.GetWalletBalanceRequest) returns (chain.gateway.v1.GetWalletBalanceResponse);
|
|
||||||
|
|
||||||
rpc SubmitTransfer(chain.gateway.v1.SubmitTransferRequest) returns (chain.gateway.v1.SubmitTransferResponse);
|
|
||||||
rpc GetTransfer(chain.gateway.v1.GetTransferRequest) returns (chain.gateway.v1.GetTransferResponse);
|
|
||||||
rpc ListTransfers(chain.gateway.v1.ListTransfersRequest) returns (chain.gateway.v1.ListTransfersResponse);
|
|
||||||
|
|
||||||
rpc EstimateTransferFee(chain.gateway.v1.EstimateTransferFeeRequest) returns (chain.gateway.v1.EstimateTransferFeeResponse);
|
|
||||||
rpc ComputeGasTopUp(chain.gateway.v1.ComputeGasTopUpRequest) returns (chain.gateway.v1.ComputeGasTopUpResponse);
|
|
||||||
rpc EnsureGasTopUp(chain.gateway.v1.EnsureGasTopUpRequest) returns (chain.gateway.v1.EnsureGasTopUpResponse);
|
|
||||||
|
|
||||||
// Card payout gateway operations.
|
|
||||||
rpc CreateCardPayout(mntx.gateway.v1.CardPayoutRequest) returns (mntx.gateway.v1.CardPayoutResponse);
|
|
||||||
rpc GetCardPayoutStatus(mntx.gateway.v1.GetCardPayoutStatusRequest) returns (mntx.gateway.v1.GetCardPayoutStatusResponse);
|
|
||||||
rpc CreateCardTokenPayout(mntx.gateway.v1.CardTokenPayoutRequest) returns (mntx.gateway.v1.CardTokenPayoutResponse);
|
|
||||||
rpc CreateCardToken(mntx.gateway.v1.CardTokenizeRequest) returns (mntx.gateway.v1.CardTokenizeResponse);
|
|
||||||
rpc ListGatewayInstances(mntx.gateway.v1.ListGatewayInstancesRequest) returns (mntx.gateway.v1.ListGatewayInstancesResponse);
|
|
||||||
|
|
||||||
// Ledger operations.
|
|
||||||
rpc CreateAccount(ledger.v1.CreateAccountRequest) returns (ledger.v1.CreateAccountResponse);
|
|
||||||
rpc ListAccounts(ledger.v1.ListAccountsRequest) returns (ledger.v1.ListAccountsResponse);
|
|
||||||
rpc PostCreditWithCharges(ledger.v1.PostCreditRequest) returns (ledger.v1.PostResponse);
|
|
||||||
rpc PostDebitWithCharges(ledger.v1.PostDebitRequest) returns (ledger.v1.PostResponse);
|
|
||||||
rpc TransferInternal(ledger.v1.TransferRequest) returns (ledger.v1.PostResponse);
|
|
||||||
rpc ApplyFXWithCharges(ledger.v1.FXRequest) returns (ledger.v1.PostResponse);
|
|
||||||
|
|
||||||
rpc GetBalance(ledger.v1.GetBalanceRequest) returns (ledger.v1.BalanceResponse);
|
|
||||||
rpc GetJournalEntry(ledger.v1.GetEntryRequest) returns (ledger.v1.JournalEntryResponse);
|
|
||||||
rpc GetStatement(ledger.v1.GetStatementRequest) returns (ledger.v1.StatementResponse);
|
|
||||||
}
|
|
||||||
@@ -116,10 +116,10 @@ if [ -f "${PROTO_DIR}/gateway/mntx/v1/mntx.proto" ]; then
|
|||||||
generate_go_with_grpc "${PROTO_DIR}/gateway/mntx/v1/mntx.proto"
|
generate_go_with_grpc "${PROTO_DIR}/gateway/mntx/v1/mntx.proto"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${PROTO_DIR}/gateway/unified/v1/gateway.proto" ]; then
|
if [ -f "${PROTO_DIR}/connector/v1/connector.proto" ]; then
|
||||||
info "Compiling unified gateway protos"
|
info "Compiling connector protos"
|
||||||
clean_pb_files "./pkg/proto/gateway/unified"
|
clean_pb_files "./pkg/proto/connector"
|
||||||
generate_go_with_grpc "${PROTO_DIR}/gateway/unified/v1/gateway.proto"
|
generate_go_with_grpc "${PROTO_DIR}/connector/v1/connector.proto"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${PROTO_DIR}/payments/orchestrator/v1/orchestrator.proto" ]; then
|
if [ -f "${PROTO_DIR}/payments/orchestrator/v1/orchestrator.proto" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user