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 } }