cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

api-wire-gateway.go (19290B)


      1 // This file is part of taler-cashless2ecash.
      2 // Copyright (C) 2024 Joel Häberli
      3 //
      4 // taler-cashless2ecash is free software: you can redistribute it and/or modify it
      5 // under the terms of the GNU Affero General Public License as published
      6 // by the Free Software Foundation, either version 3 of the License,
      7 // or (at your option) any later version.
      8 //
      9 // taler-cashless2ecash is distributed in the hope that it will be useful, but
     10 // WITHOUT ANY WARRANTY; without even the implied warranty of
     11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     12 // Affero General Public License for more details.
     13 //
     14 // You should have received a copy of the GNU Affero General Public License
     15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 //
     17 // SPDX-License-Identifier: AGPL3.0-or-later
     18 
     19 package internal_api
     20 
     21 import (
     22 	"bytes"
     23 	internal_utils "c2ec/internal/utils"
     24 	"c2ec/pkg/config"
     25 	"c2ec/pkg/db"
     26 	"c2ec/pkg/provider"
     27 	"errors"
     28 	"fmt"
     29 	"log"
     30 	"net/http"
     31 	"strconv"
     32 	"time"
     33 )
     34 
     35 const INCOMING_RESERVE_TRANSACTION_TYPE = "RESERVE"
     36 
     37 // https://docs.taler.net/core/api-bank-wire.html#tsref-type-WireConfig
     38 type WireConfig struct {
     39 	Name           string `json:"name"`
     40 	Version        string `json:"version"`
     41 	Currency       string `json:"currency"`
     42 	Implementation string `json:"implementation"`
     43 }
     44 
     45 // https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferRequest
     46 type TransferRequest struct {
     47 	RequestUid      string `json:"request_uid"`
     48 	Amount          string `json:"amount"`
     49 	ExchangeBaseUrl string `json:"exchange_base_url"`
     50 	Wtid            string `json:"wtid"`
     51 	CreditAccount   string `json:"credit_account"`
     52 }
     53 
     54 // https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferResponse
     55 type TransferResponse struct {
     56 	Timestamp internal_utils.Timestamp `json:"timestamp"`
     57 	RowId     int                      `json:"row_id"`
     58 }
     59 
     60 // https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory
     61 type IncomingHistory struct {
     62 	IncomingTransactions []IncomingReserveTransaction `json:"incoming_transactions"`
     63 	CreditAccount        string                       `json:"credit_account"`
     64 }
     65 
     66 // type RESERVE | https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingReserveTransaction
     67 type IncomingReserveTransaction struct {
     68 	Type         string                   `json:"type"`
     69 	RowId        int                      `json:"row_id"`
     70 	Date         internal_utils.Timestamp `json:"date"`
     71 	Amount       string                   `json:"amount"`
     72 	DebitAccount string                   `json:"debit_account"`
     73 	ReservePub   string                   `json:"reserve_pub"`
     74 }
     75 
     76 type OutgoingHistory struct {
     77 	OutgoingTransactions []*OutgoingBankTransaction `json:"outgoing_transactions"`
     78 	DebitAccount         string                     `json:"debit_account"`
     79 }
     80 
     81 type OutgoingBankTransaction struct {
     82 	RowId           uint64                       `json:"row_id"`
     83 	Date            internal_utils.Timestamp     `json:"date"`
     84 	Amount          string                       `json:"amount"`
     85 	CreditAccount   string                       `json:"credit_account"`
     86 	Wtid            internal_utils.ShortHashCode `json:"wtid"`
     87 	ExchangeBaseUrl string                       `json:"exchange_base_url"`
     88 }
     89 
     90 func NewIncomingReserveTransaction(w *db.Withdrawal) *IncomingReserveTransaction {
     91 
     92 	if w == nil {
     93 		internal_utils.LogWarn("wire-gateway", "the withdrawal was nil")
     94 		return nil
     95 	}
     96 
     97 	prvdr, err := db.DB.GetProviderByTerminal(w.TerminalId)
     98 	if err != nil {
     99 		internal_utils.LogError("wire-gateway", err)
    100 		return nil
    101 	}
    102 
    103 	client := provider.PROVIDER_CLIENTS[prvdr.Name]
    104 	if client == nil {
    105 		internal_utils.LogError("wire-gateway", errors.New("no provider client with name="+prvdr.Name))
    106 		return nil
    107 	}
    108 
    109 	t := new(IncomingReserveTransaction)
    110 	a, err := internal_utils.ToAmount(w.Amount)
    111 	if err != nil {
    112 		internal_utils.LogError("wire-gateway", err)
    113 		return nil
    114 	}
    115 	t.Amount = internal_utils.FormatAmount(a, config.CONFIG.Server.CurrencyFractionDigits)
    116 	t.Date = internal_utils.Timestamp{
    117 		Ts: int(w.RegistrationTs),
    118 	}
    119 	t.DebitAccount = client.FormatPayto(w)
    120 	t.ReservePub = internal_utils.FormatEddsaPubKey(w.ReservePubKey)
    121 	if w.ConfirmedRowId == nil {
    122 		internal_utils.LogError("wire-gateway", fmt.Errorf("expected non-nil confirmed_row_id for withdrawal_row_id=%d", w.WithdrawalRowId))
    123 		return nil
    124 	}
    125 	t.RowId = int(*w.ConfirmedRowId)
    126 	t.Type = INCOMING_RESERVE_TRANSACTION_TYPE
    127 	return t
    128 }
    129 
    130 func NewOutgoingBankTransaction(tr *db.Transfer) *OutgoingBankTransaction {
    131 	t := new(OutgoingBankTransaction)
    132 	a, err := internal_utils.ToAmount(tr.Amount)
    133 	if err != nil {
    134 		internal_utils.LogError("wire-gateway", err)
    135 		return nil
    136 	}
    137 	t.Amount = internal_utils.FormatAmount(a, config.CONFIG.Server.CurrencyFractionDigits)
    138 	t.Date = internal_utils.Timestamp{
    139 		Ts: int(tr.TransferTs),
    140 	}
    141 	t.CreditAccount = tr.CreditAccount
    142 	t.ExchangeBaseUrl = tr.ExchangeBaseUrl
    143 	if tr.TransferredRowId == nil {
    144 		internal_utils.LogError("wire-gateway", fmt.Errorf("expected non-nil transferred_row_id for row_id=%d", tr.RowId))
    145 		return nil
    146 	}
    147 	t.RowId = uint64(*tr.TransferredRowId)
    148 	t.Wtid = internal_utils.ShortHashCode(tr.Wtid)
    149 	return t
    150 }
    151 
    152 func WireGatewayConfig(res http.ResponseWriter, req *http.Request) {
    153 
    154 	cfg := WireConfig{
    155 		Name:           "taler-wire-gateway",
    156 		Currency:       config.CONFIG.Server.Currency,
    157 		Version:        "0:0:1",
    158 		Implementation: "",
    159 	}
    160 
    161 	serializedCfg, err := internal_utils.NewJsonCodec[WireConfig]().EncodeToBytes(&cfg)
    162 	if err != nil {
    163 		log.Default().Printf("failed serializing config: %s", err.Error())
    164 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    165 		res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    166 		return
    167 	}
    168 
    169 	internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_OK)
    170 	res.WriteHeader(internal_utils.HTTP_OK)
    171 	res.Write(serializedCfg)
    172 }
    173 
    174 func Transfer(res http.ResponseWriter, req *http.Request) {
    175 
    176 	auth := AuthenticateWirewatcher(req)
    177 	if !auth {
    178 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_UNAUTHORIZED)
    179 		res.WriteHeader(internal_utils.HTTP_UNAUTHORIZED)
    180 		return
    181 	}
    182 
    183 	jsonCodec := internal_utils.NewJsonCodec[TransferRequest]()
    184 	transfer, err := internal_utils.ReadStructFromBody[TransferRequest](req, jsonCodec)
    185 	if err != nil {
    186 		internal_utils.LogError("wire-gateway-api", err)
    187 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST)
    188 		res.WriteHeader(internal_utils.HTTP_BAD_REQUEST)
    189 		return
    190 	}
    191 
    192 	if transfer.Amount == "" || transfer.CreditAccount == "" || transfer.RequestUid == "" {
    193 		internal_utils.LogError("wire-gateway-api", errors.New("invalid request"))
    194 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST)
    195 		res.WriteHeader(internal_utils.HTTP_BAD_REQUEST)
    196 		return
    197 	}
    198 
    199 	paytoTargetType, tid, err := internal_utils.ParsePaytoUri(transfer.CreditAccount)
    200 	internal_utils.LogInfo("wire-gateway-api", fmt.Sprintf("parsed payto-target-type='%s'", paytoTargetType))
    201 	if err != nil {
    202 		internal_utils.LogError("wire-gateway-api", err)
    203 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST)
    204 		res.WriteHeader(internal_utils.HTTP_BAD_REQUEST)
    205 		return
    206 	}
    207 
    208 	p, err := db.DB.GetTerminalProviderByPaytoTargetType(paytoTargetType)
    209 	if err != nil {
    210 		internal_utils.LogWarn("wire-gateway-api", "unable to find provider for provider-target-type="+paytoTargetType)
    211 		internal_utils.LogError("wire-gateway-api", err)
    212 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    213 		res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    214 		return
    215 	}
    216 
    217 	decodedRequestUid := bytes.NewBufferString(transfer.RequestUid).Bytes()
    218 	t, err := db.DB.GetTransferById(decodedRequestUid)
    219 	if err != nil {
    220 		internal_utils.LogWarn("wire-gateway-api", "failed retrieving transfer for requestUid="+transfer.RequestUid)
    221 		internal_utils.LogError("wire-gateway-api", err)
    222 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    223 		res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    224 		return
    225 	}
    226 
    227 	if t == nil {
    228 
    229 		// limitation: currently only full refunds are implemented.
    230 		//   this means that we also check that no other transaction
    231 		//   to the same recipient with this credit_account is present.
    232 		transfers, err := db.DB.GetTransfersByCreditAccount(transfer.CreditAccount)
    233 		if err != nil {
    234 			internal_utils.LogWarn("wire-gateway-api", "looking for transfers with the credit account failed")
    235 			internal_utils.LogError("wire-gateway-api", err)
    236 			internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    237 			res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    238 			return
    239 		}
    240 
    241 		if len(transfers) > 0 {
    242 			//   when the withdrawal was already refunded we act like everything is
    243 			//   ok, because the transfer was registered earlier and the customer
    244 			//   will get their money back (or already have). The Exchange will
    245 			//	 not loose money on the other hand because the refund is done twice.
    246 			internal_utils.LogWarn("wire-gateway-api", "full refunds only limitation")
    247 			internal_utils.LogError("wire-gateway-api", fmt.Errorf("currently only full refunds are supported. Withdrawal %s already refunded", transfer.CreditAccount))
    248 			internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_OK)
    249 			res.WriteHeader(internal_utils.HTTP_OK)
    250 			return
    251 		}
    252 
    253 		// no transfer for this request_id -> generate new
    254 		amount, err := internal_utils.ParseAmount(transfer.Amount, config.CONFIG.Server.CurrencyFractionDigits)
    255 		if err != nil {
    256 			internal_utils.LogWarn("wire-gateway-api", "failed parsing amount")
    257 			internal_utils.LogError("wire-gateway-api", err)
    258 			internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    259 			res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    260 			return
    261 		}
    262 		err = db.DB.AddTransfer(
    263 			decodedRequestUid,
    264 			amount,
    265 			transfer.ExchangeBaseUrl,
    266 			string(transfer.Wtid),
    267 			transfer.CreditAccount,
    268 			time.Now(),
    269 		)
    270 		if err != nil {
    271 			internal_utils.LogWarn("wire-gateway-api", "failed adding new transfer entry to database")
    272 			internal_utils.LogError("wire-gateway-api", err)
    273 			internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    274 			res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    275 			return
    276 		}
    277 	} else {
    278 
    279 		// check that the wanted provider is configured.
    280 		refundClient := provider.PROVIDER_CLIENTS[p.Name]
    281 		if refundClient == nil {
    282 			internal_utils.LogError("wire-gateway-api", errors.New("client for provider "+p.Name+" not initialized"))
    283 			internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    284 			res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    285 			return
    286 		}
    287 
    288 		// the transfer is only processed if the body matches.
    289 		ta, err := internal_utils.ToAmount(t.Amount)
    290 		if err != nil {
    291 			internal_utils.LogError("wire-gateway-api", err)
    292 			internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    293 			res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    294 			return
    295 		}
    296 		if transfer.Amount != internal_utils.FormatAmount(ta, config.CONFIG.Server.CurrencyFractionDigits) ||
    297 			transfer.ExchangeBaseUrl != t.ExchangeBaseUrl ||
    298 			transfer.Wtid != t.Wtid ||
    299 			transfer.CreditAccount != t.CreditAccount {
    300 
    301 			internal_utils.LogWarn("wire-gateway-api", "idempotency violation")
    302 			internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_CONFLICT)
    303 			res.WriteHeader(internal_utils.HTTP_CONFLICT)
    304 			return
    305 		}
    306 
    307 		w, err := db.DB.GetWithdrawalByProviderTransactionId(tid)
    308 		if err != nil || w == nil {
    309 			internal_utils.LogWarn("wire-gateway-api", "unable to find withdrawal with given provider transaction id")
    310 			internal_utils.LogError("wire-gateway-api", err)
    311 			internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    312 			res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    313 			return
    314 		}
    315 	}
    316 	internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_OK)
    317 }
    318 
    319 // :query start: *Optional.*
    320 //
    321 //	Row identifier to explicitly set the *starting point* of the query.
    322 //
    323 // :query delta:
    324 //
    325 //	The *delta* value that determines the range of the query.
    326 //
    327 // :query long_poll_ms: *Optional.*
    328 //
    329 //	If this parameter is specified and the result of the query would be empty,
    330 //	the bank will wait up to ``long_poll_ms`` milliseconds for new transactions
    331 //	that match the query to arrive and only then send the HTTP response.
    332 //	A client must never rely on this behavior, as the bank may return a response
    333 //	immediately or after waiting only a fraction of ``long_poll_ms``.
    334 func HistoryIncoming(res http.ResponseWriter, req *http.Request) {
    335 
    336 	auth := AuthenticateWirewatcher(req)
    337 	if !auth {
    338 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_UNAUTHORIZED)
    339 		res.WriteHeader(internal_utils.HTTP_UNAUTHORIZED)
    340 		return
    341 	}
    342 
    343 	// read and validate request query parameters
    344 	timeOfReq := time.Now()
    345 	shouldStartLongPoll := true
    346 	var longPollMilli int
    347 	if longPollMilliPtr, accepted := internal_utils.AcceptOptionalParamOrWriteResponse(
    348 		"long_poll_ms", strconv.Atoi, req, res,
    349 	); accepted {
    350 		if longPollMilliPtr != nil {
    351 			longPollMilli = *longPollMilliPtr
    352 		} else {
    353 			// this means parameter was not given.
    354 			// no long polling (simple get)
    355 			shouldStartLongPoll = false
    356 		}
    357 	}
    358 
    359 	var start = 0 // read most recent entries by default
    360 	if startPtr, accepted := internal_utils.AcceptOptionalParamOrWriteResponse(
    361 		"start", strconv.Atoi, req, res,
    362 	); accepted {
    363 		if startPtr != nil {
    364 			start = *startPtr
    365 		}
    366 	} else {
    367 		res.Header().Add(internal_utils.CONTENT_TYPE_HEADER, "application/json")
    368 		internal_utils.LogWarn("wire-gateway-api", "invalid parameter")
    369 		return
    370 	}
    371 
    372 	var delta = 0
    373 	if deltaPtr, accepted := internal_utils.AcceptOptionalParamOrWriteResponse(
    374 		"delta", strconv.Atoi, req, res,
    375 	); accepted {
    376 		if deltaPtr != nil {
    377 			delta = *deltaPtr
    378 		}
    379 	} else {
    380 		res.Header().Add(internal_utils.CONTENT_TYPE_HEADER, "application/json")
    381 		internal_utils.LogWarn("wire-gateway-api", "invalid parameter")
    382 		return
    383 	}
    384 
    385 	if delta == 0 {
    386 		delta = 10
    387 	}
    388 
    389 	if shouldStartLongPoll {
    390 
    391 		// this will just wait / block until the milliseconds are exceeded.
    392 		time.Sleep(time.Duration(longPollMilli) * time.Millisecond)
    393 	}
    394 
    395 	withdrawals, err := db.DB.GetConfirmedWithdrawals(start, delta, timeOfReq)
    396 
    397 	if err != nil {
    398 		internal_utils.LogError("wire-gateway-api", err)
    399 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    400 		res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    401 		return
    402 	}
    403 
    404 	if len(withdrawals) < 1 {
    405 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_NO_CONTENT)
    406 		res.WriteHeader(internal_utils.HTTP_NO_CONTENT)
    407 		return
    408 	}
    409 
    410 	transactions := make([]IncomingReserveTransaction, 0)
    411 	for _, w := range withdrawals {
    412 		if w.Amount.Val == 0 && w.Amount.Frac == 0 {
    413 			internal_utils.LogInfo("wire-gateway-api", "ignoring zero amount withdrawal")
    414 			continue
    415 		}
    416 		if w.ReservePubKey == nil || len(w.ReservePubKey) == 0 {
    417 			internal_utils.LogWarn("wire-gateway-api", "ignoring confirmed withdrawal with no reserve public key (probably a test transaction)")
    418 			continue
    419 		}
    420 		transaction := NewIncomingReserveTransaction(w)
    421 		if transaction != nil {
    422 			transactions = append(transactions, *transaction)
    423 		}
    424 	}
    425 
    426 	hist := IncomingHistory{
    427 		IncomingTransactions: transactions,
    428 		CreditAccount:        config.CONFIG.Server.CreditAccount,
    429 	}
    430 
    431 	encoder := internal_utils.NewJsonCodec[IncomingHistory]()
    432 	enc, err := encoder.EncodeToBytes(&hist)
    433 	if err != nil {
    434 		internal_utils.LogError("wire-gateway-api", err)
    435 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    436 		res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    437 		return
    438 	}
    439 
    440 	res.Header().Add(internal_utils.CONTENT_TYPE_HEADER, encoder.HttpApplicationContentHeader())
    441 	internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_OK)
    442 	res.WriteHeader(internal_utils.HTTP_OK)
    443 	res.Write(enc)
    444 }
    445 
    446 func HistoryOutgoing(res http.ResponseWriter, req *http.Request) {
    447 
    448 	auth := AuthenticateWirewatcher(req)
    449 	if !auth {
    450 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_UNAUTHORIZED)
    451 		res.WriteHeader(internal_utils.HTTP_UNAUTHORIZED)
    452 		return
    453 	}
    454 
    455 	// read and validate request query parameters
    456 	timeOfReq := time.Now()
    457 	shouldStartLongPoll := true
    458 	var longPollMilli int
    459 	if longPollMilliPtr, accepted := internal_utils.AcceptOptionalParamOrWriteResponse(
    460 		"long_poll_ms", strconv.Atoi, req, res,
    461 	); accepted {
    462 	} else {
    463 		if longPollMilliPtr != nil {
    464 			longPollMilli = *longPollMilliPtr
    465 		} else {
    466 			// this means parameter was not given.
    467 			// no long polling (simple get)
    468 			shouldStartLongPoll = false
    469 		}
    470 	}
    471 
    472 	var start int
    473 	if startPtr, accepted := internal_utils.AcceptOptionalParamOrWriteResponse(
    474 		"start", strconv.Atoi, req, res,
    475 	); accepted {
    476 	} else {
    477 		if startPtr != nil {
    478 			start = *startPtr
    479 		}
    480 	}
    481 
    482 	var delta int
    483 	if deltaPtr, accepted := internal_utils.AcceptOptionalParamOrWriteResponse(
    484 		"delta", strconv.Atoi, req, res,
    485 	); accepted {
    486 	} else {
    487 		if deltaPtr != nil {
    488 			delta = *deltaPtr
    489 		}
    490 	}
    491 
    492 	if delta == 0 {
    493 		delta = 10
    494 	}
    495 
    496 	if shouldStartLongPoll {
    497 
    498 		// this will just wait / block until the milliseconds are exceeded.
    499 		time.Sleep(time.Duration(longPollMilli) * time.Millisecond)
    500 	}
    501 
    502 	transfers, err := db.DB.GetTransfers(start, delta, timeOfReq)
    503 
    504 	if err != nil {
    505 		internal_utils.LogError("wire-gateway-api", err)
    506 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    507 		res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    508 		return
    509 	}
    510 
    511 	filtered := make([]*db.Transfer, 0)
    512 	for _, t := range transfers {
    513 		if t.Status == 0 {
    514 			// only consider transfer which were successful
    515 			filtered = append(filtered, t)
    516 		}
    517 	}
    518 
    519 	if len(filtered) < 1 {
    520 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_NO_CONTENT)
    521 		res.WriteHeader(internal_utils.HTTP_NO_CONTENT)
    522 		return
    523 	}
    524 
    525 	transactions := make([]*OutgoingBankTransaction, len(filtered))
    526 	for _, t := range filtered {
    527 		transactions = append(transactions, NewOutgoingBankTransaction(t))
    528 	}
    529 	transactions = internal_utils.RemoveNulls(transactions)
    530 
    531 	outgoingHistory := OutgoingHistory{
    532 		OutgoingTransactions: transactions,
    533 		DebitAccount:         config.CONFIG.Server.CreditAccount,
    534 	}
    535 	encoder := internal_utils.NewJsonCodec[OutgoingHistory]()
    536 	enc, err := encoder.EncodeToBytes(&outgoingHistory)
    537 	if err != nil {
    538 		internal_utils.LogError("wire-gateway-api", err)
    539 		internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    540 		res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR)
    541 		return
    542 	}
    543 
    544 	res.Header().Add(internal_utils.CONTENT_TYPE_HEADER, encoder.HttpApplicationContentHeader())
    545 	internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_OK)
    546 	res.WriteHeader(internal_utils.HTTP_OK)
    547 	res.Write(enc)
    548 }
    549 
    550 // This method is currently dead and implemented for API conformance
    551 func AdminAddIncoming(res http.ResponseWriter, req *http.Request) {
    552 
    553 	// not implemented, because not used
    554 	internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_NOT_IMPLEMENTED)
    555 	res.WriteHeader(internal_utils.HTTP_NOT_IMPLEMENTED)
    556 }