254 lines
11 KiB
Go
254 lines
11 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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, merrors.InvalidArgument("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, merrors.InvalidArgument("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
|
|
}
|
|
}
|