taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

api-bank-wire.rst (23540B)


      1 ..
      2   This file is part of GNU TALER.
      3   Copyright (C) 2019-2025 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 2.1, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 
     16 .. _taler-wire-gateway-http-api:
     17 
     18 ===========================
     19 Taler Wire Gateway HTTP API
     20 ===========================
     21 
     22 This section describes the API offered by the Taler wire gateway. The API is
     23 used by the exchange to trigger transactions and query incoming transactions, as
     24 well as by the auditor to query incoming and outgoing transactions.
     25 
     26 This API is currently implemented by the Taler Demo Bank, as well as by
     27 LibEuFin.
     28 
     29 .. http:get:: /config
     30 
     31   Return the protocol version and configuration information about the bank.
     32   This specification corresponds to ``current`` protocol being version **4**.
     33 
     34   **Response:**
     35 
     36   :http:statuscode:`200 OK`:
     37     The exchange responds with a `WireConfig` object. This request should
     38     virtually always be successful.
     39 
     40   **Details:**
     41 
     42   .. ts:def:: WireConfig
     43 
     44     interface WireConfig {
     45       // Name of the API.
     46       name: "taler-wire-gateway";
     47 
     48       // libtool-style representation of the Bank protocol version, see
     49       // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
     50       // The format is "current:revision:age".
     51       version: string;
     52 
     53       // Currency used by this gateway.
     54       currency: string;
     55 
     56       // URN of the implementation (needed to interpret 'revision' in version).
     57       // @since v0, may become mandatory in the future.
     58       implementation?: string;
     59 
     60       // Wether implementation support account existence check
     61       support_account_check: boolean;
     62     }
     63 
     64 --------------
     65 Authentication
     66 --------------
     67 
     68 The bank library authenticates requests to the wire gateway via
     69 `HTTP basic auth <https://tools.ietf.org/html/rfc7617>`_.
     70 
     71 -------------------
     72 Making Transactions
     73 -------------------
     74 
     75 .. http:post:: /transfer
     76 
     77   Initiate a new wire transfer from the exchange's bank account, typically to a
     78   merchant.
     79 
     80   The exchange's bank account is not included in the request, but instead
     81   derived from the username in the ``Authorization`` header and/or the request
     82   base URL.
     83 
     84   To make the API idempotent, the client must include a nonce. Requests with
     85   the same nonce are rejected unless the request is the same.
     86 
     87   **Request:**
     88 
     89   .. ts:def:: TransferRequest
     90 
     91     interface TransferRequest {
     92       // Nonce to make the request idempotent.  Requests with the same
     93       // ``request_uid`` that differs in any of the other fields
     94       // are rejected.
     95       request_uid: HashCode;
     96 
     97       // Amount to transfer.
     98       amount: Amount;
     99 
    100       // Base URL of the exchange.  Shall be included by the bank gateway
    101       // in the appropriate section of the wire transfer details.
    102       exchange_base_url: string;
    103 
    104       // Wire transfer identifier chosen by the exchange,
    105       // used by the merchant to identify the Taler order(s)
    106       // associated with this wire transfer.
    107       wtid: ShortHashCode;
    108 
    109       // The recipient's account identifier as a full payto URI.
    110       credit_account: string;
    111     }
    112 
    113   **Response:**
    114 
    115   :http:statuscode:`200 OK`:
    116     The request has been correctly handled, so the funds have been transferred to
    117     the recipient's account.  The body is a `TransferResponse`.
    118   :http:statuscode:`400 Bad request`:
    119     Request malformed. The bank replies with an `ErrorDetail` object.
    120   :http:statuscode:`401 Unauthorized`:
    121     Authentication failed, likely the credentials are wrong.
    122   :http:statuscode:`404 Not found`:
    123     The endpoint is wrong or the user name is unknown. The bank replies with an `ErrorDetail` object.
    124   :http:statuscode:`409 Conflict`:
    125     * ``TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED``: an operation with the same ``request_uid`` but different details has been submitted before.
    126     * ``TALER_EC_BANK_TRANSFER_WTID_REUSED``: an operation with the same ``wtid`` but a different ``request_uid`` has been submitted before.
    127 
    128   **Details:**
    129 
    130   .. ts:def:: TransferResponse
    131 
    132     interface TransferResponse {
    133       // Timestamp that indicates when the wire transfer will be executed.
    134       // In cases where the wire transfer gateway is unable to know when
    135       // the wire transfer will be executed, the time at which the request
    136       // has been received and stored will be returned.
    137       // The purpose of this field is for debugging (humans trying to find
    138       // the transaction) as well as for taxation (determining which
    139       // time period a transaction belongs to).
    140       timestamp: Timestamp;
    141 
    142       // Opaque ID of the wire transfer initiation performed by the bank.
    143       // It is different from the /history endpoints row_id.
    144       row_id: SafeUint64;
    145     }
    146 
    147 .. http:get:: /transfers
    148 
    149   Return a list of transfers initiated from the exchange.
    150 
    151   The bank account of the exchange is determined via the base URL and/or the
    152   user name in the ``Authorization`` header. The transfer history
    153   might come from a "virtual" account, where multiple real bank accounts are
    154   merged into one history.
    155 
    156   Since protocol **v3**.
    157 
    158   **Request:**
    159 
    160   :query limit: *Optional.*
    161     At most return the given number of results. Negative for descending by
    162     ``row_id``, positive for ascending by ``row_id``. Defaults to ``-20``.
    163   :query offset: *Optional.*
    164     Starting ``row_id`` for :ref:`pagination <row-id-pagination>`.
    165   :query status: *Optional*.
    166     Filters by status.
    167 
    168   **Response:**
    169 
    170   :http:statuscode:`200 OK`:
    171     JSON object of type `TransferList`.
    172   :http:statuscode:`204 No content`:
    173     There are no transfers to report (under the given filter).
    174   :http:statuscode:`400 Bad request`:
    175     Request malformed.
    176   :http:statuscode:`401 Unauthorized`:
    177     Authentication failed, likely the credentials are wrong.
    178   :http:statuscode:`404 Not found`:
    179     The endpoint is wrong or the user name is unknown.
    180 
    181   **Details:**
    182 
    183   .. ts:def:: TransferList
    184 
    185     interface TransferList {
    186       // Array of initiated transfers.
    187       transfers: TransferListStatus[];
    188 
    189       // Full payto:// URI to identify the sender of funds.
    190       // This must be one of the exchange's bank accounts.
    191       // Credit account is shared by all incoming transactions
    192       // as per the nature of the request.
    193       debit_account: string;
    194     }
    195 
    196   .. ts:def:: TransferListStatus
    197 
    198     interface TransferListStatus {
    199       // Opaque ID of the wire transfer initiation performed by the bank.
    200       // It is different from the /history endpoints row_id.
    201       row_id: SafeUint64;
    202 
    203       // Current status of the transfer
    204       // pending: the transfer is in progress
    205       // transient_failure: the transfer has failed but may succeed later
    206       // permanent_failure: the transfer has failed permanently and will never appear in the outgoing history
    207       // success: the transfer has succeeded  and appears in the outgoing history
    208       status: "pending" | "transient_failure" | "permanent_failure" | "success";
    209 
    210       // Amount to transfer.
    211       amount: Amount;
    212 
    213       // The recipient's account identifier as a full payto:// URI.
    214       credit_account: string;
    215 
    216       // Timestamp that indicates when the wire transfer was executed.
    217       // In cases where the wire transfer gateway is unable to know when
    218       // the wire transfer will be executed, the time at which the request
    219       // has been received and stored will be returned.
    220       // The purpose of this field is for debugging (humans trying to find
    221       // the transaction) as well as for taxation (determining which
    222       // time period a transaction belongs to).
    223       timestamp: Timestamp;
    224     }
    225 
    226 
    227 .. http:get:: /transfers/$ROW_ID
    228 
    229   Return the status of a transfer initiated from the exchange, identified by the ``ROW_ID``.
    230 
    231   Since protocol **v3**.
    232 
    233   **Response:**
    234 
    235   :http:statuscode:`200 OK`:
    236     The transfer is known, and details are given in the `TransferStatus` response body.
    237   :http:statuscode:`400 Bad request`:
    238     Request malformed.
    239   :http:statuscode:`401 Unauthorized`:
    240     Authentication failed, likely the credentials are wrong.
    241   :http:statuscode:`404 Not found`:
    242     The transfer was not found.
    243 
    244   **Details:**
    245 
    246   .. ts:def:: TransferStatus
    247 
    248     interface TransferStatus {
    249       // Current status of the transfer
    250       // pending: the transfer is in progress
    251       // transient_failure: the transfer has failed but may succeed later
    252       // permanent_failure: the transfer has failed permanently and will never appear in the outgoing history
    253       // success: the transfer has succeeded  and appears in the outgoing history
    254       status: "pending" | "transient_failure" | "permanent_failure" | "success";
    255 
    256       // Optional unstructured messages about the transfer's status. Can be used to document the reasons for failure or the state of progress.
    257       status_msg?: string;
    258 
    259       // Amount to transfer.
    260       amount: Amount;
    261 
    262       // Base URL of the exchange.  Shall be included by the bank gateway
    263       // in the appropriate section of the wire transfer details.
    264       exchange_base_url: string;
    265 
    266       // Wire transfer identifier chosen by the exchange,
    267       // used by the merchant to identify the Taler order(s)
    268       // associated with this wire transfer.
    269       wtid: ShortHashCode;
    270 
    271       // The recipient's account identifier as a full payto URI.
    272       credit_account: string;
    273 
    274       // Timestamp that indicates when the wire transfer was executed.
    275       // In cases where the wire transfer gateway is unable to know when
    276       // the wire transfer will be executed, the time at which the request
    277       // has been received and stored will be returned.
    278       // The purpose of this field is for debugging (humans trying to find
    279       // the transaction) as well as for taxation (determining which
    280       // time period a transaction belongs to).
    281       timestamp: Timestamp;
    282     }
    283 
    284 --------------------------------
    285 Querying the transaction history
    286 --------------------------------
    287 
    288 The exchange's bank account is derived from the username in the
    289 ``Authorization`` header and/or the request's base URL. In fact, the
    290 transaction history may come from a "virtual" account, where several real bank
    291 accounts are merged into a single history.
    292 
    293 .. http:get:: /history/incoming
    294 
    295   Return a list of transactions made from or to the exchange.
    296 
    297   Incoming transactions must contain a valid reserve public key.  If a bank
    298   transaction does not conform to the right syntax, the wire gateway must not
    299   report it to the exchange, and send funds back to the sender if possible.
    300 
    301   **Request:**
    302 
    303   :query limit: *Optional.*
    304     At most return the given number of results. Negative for descending by ``row_id``, positive for ascending by ``row_id``. Defaults to ``-20``. Since protocol **v2**.
    305   :query offset: *Optional.*
    306     Starting ``row_id`` for :ref:`pagination <row-id-pagination>`. Since protocol **v2**.
    307   :query timeout_ms: *Optional.*
    308     Timeout in milliseconds, for :ref:`long-polling <long-polling>`, to wait for at least one element to be shown. Only useful if *limit* is positive. Since protocol **v2**.
    309   :query delta: *Optional.*
    310     Deprecated in protocol **v2**. Use *limit* instead.
    311   :query start: *Optional.*
    312     Deprecated in protocol **v2**. Use *offset* instead.
    313   :query long_poll_ms: *Optional.*
    314     Deprecated in protocol **v2**. Use *timeout_ms* instead.
    315 
    316   **Response:**
    317 
    318   :http:statuscode:`200 OK`:
    319      JSON object of type `IncomingHistory`.
    320   :http:statuscode:`204 No content`:
    321     There are not transactions to report (under the given filter).
    322   :http:statuscode:`400 Bad request`:
    323      Request malformed. The bank replies with an `ErrorDetail` object.
    324   :http:statuscode:`401 Unauthorized`:
    325      Authentication failed, likely the credentials are wrong.
    326   :http:statuscode:`404 Not found`:
    327      The endpoint is wrong or the user name is unknown. The bank replies with an `ErrorDetail` object.
    328 
    329   **Details:**
    330 
    331   .. ts:def:: IncomingHistory
    332 
    333     interface IncomingHistory {
    334       // Array of incoming transactions.
    335       incoming_transactions: IncomingBankTransaction[];
    336 
    337       // Full payto URI to identify the receiver of funds.
    338       // This must be one of the exchange's bank accounts.
    339       // Credit account is shared by all incoming transactions
    340       // as per the nature of the request.
    341       credit_account: string;
    342     }
    343 
    344   .. ts:def:: IncomingBankTransaction
    345 
    346     // Union discriminated by the "type" field.
    347     type IncomingBankTransaction =
    348     | IncomingKycAuthTransaction
    349     | IncomingReserveTransaction
    350     | IncomingWadTransaction;
    351 
    352   .. ts:def:: IncomingKycAuthTransaction
    353 
    354     // Since protocol **v1**.
    355     interface IncomingKycAuthTransaction {
    356       type: "KYCAUTH";
    357 
    358       // Opaque identifier of the returned record.
    359       row_id: SafeUint64;
    360 
    361       // Date of the transaction.
    362       date: Timestamp;
    363 
    364       // Amount received before credit_fee.
    365       amount: Amount;
    366 
    367       // Fee paid by the creditor.
    368       // If not null, creditor actually received amount - credit_fee
    369       // @since **v3**
    370       credit_fee?: Amount;
    371 
    372       // Full payto URI to identify the sender of funds.
    373       debit_account: string;
    374 
    375       // The account public key extracted from the transaction details.
    376       account_pub: EddsaPublicKey;
    377     }
    378 
    379   .. ts:def:: IncomingReserveTransaction
    380 
    381     interface IncomingReserveTransaction {
    382       type: "RESERVE";
    383 
    384       // Opaque identifier of the returned record.
    385       row_id: SafeUint64;
    386 
    387       // Date of the transaction.
    388       date: Timestamp;
    389 
    390       // Amount received before credit_fee.
    391       amount: Amount;
    392 
    393       // Fee payed by the creditor.
    394       // If not null, creditor actually received amount - 
    395       // @since **v3**
    396       credit_fee?: Amount;
    397 
    398       // Full payto URI to identify the sender of funds.
    399       debit_account: string;
    400 
    401       // The reserve public key extracted from the transaction details.
    402       reserve_pub: EddsaPublicKey;
    403     }
    404 
    405   .. ts:def:: IncomingWadTransaction
    406 
    407     interface IncomingWadTransaction {
    408       type: "WAD";
    409 
    410       // Opaque identifier of the returned record.
    411       row_id: SafeUint64;
    412 
    413       // Date of the transaction.
    414       date: Timestamp;
    415 
    416       // Amount received before credit_fee.
    417       amount: Amount;
    418 
    419       // Fee payed by the creditor.
    420       // If not null, creditor actually received amount - credit_fee
    421       // @since **v3**
    422       credit_fee?: Amount;
    423 
    424       // Full payto URI to identify the sender of funds.
    425       debit_account: string;
    426 
    427       // Base URL of the exchange that originated the wad.
    428       origin_exchange_url: string;
    429 
    430       // The reserve public key extracted from the transaction details.
    431       wad_id: WadId;
    432     }
    433 
    434 
    435 .. http:get:: /history/outgoing
    436 
    437   Return a list of transactions made by the exchange, typically to a merchant.
    438 
    439   **Request:**
    440 
    441   :query limit: *Optional.*
    442     At most return the given number of results. Negative for descending by ``row_id``, positive for ascending by ``row_id``. Defaults to ``-20``. Since protocol **v2**.
    443   :query offset: *Optional.*
    444     Starting ``row_id`` for :ref:`pagination <row-id-pagination>`. Since protocol **v2**.
    445   :query timeout_ms: *Optional.*
    446     Timeout in milliseconds, for :ref:`long-polling <long-polling>`, to wait for at least one element to be shown. Only useful if *limit* is positive. Since protocol **v2**.
    447   :query delta: *Optional.*
    448     Deprecated in protocol **v2**. Use *limit* instead.
    449   :query start: *Optional.*
    450     Deprecated in protocol **v2**. Use *offset* instead.
    451   :query long_poll_ms: *Optional.*
    452     Deprecated in protocol **v2**. Use *timeout_ms* instead.
    453 
    454   **Response:**
    455 
    456   :http:statuscode:`200 OK`:
    457     JSON object of type `OutgoingHistory`.
    458   :http:statuscode:`204 No content`:
    459     There are not transactions to report (under the given filter).
    460   :http:statuscode:`400 Bad request`:
    461     Request malformed. The bank replies with an `ErrorDetail` object.
    462   :http:statuscode:`401 Unauthorized`:
    463     Authentication failed, likely the credentials are wrong.
    464   :http:statuscode:`404 Not found`:
    465     The endpoint is wrong or the user name is unknown. The bank replies with an `ErrorDetail` object.
    466 
    467   **Details:**
    468 
    469   .. ts:def:: OutgoingHistory
    470 
    471     interface OutgoingHistory {
    472       // Array of outgoing transactions.
    473       outgoing_transactions: OutgoingBankTransaction[];
    474 
    475       // Full payto URI to identify the sender of funds.
    476       // This must be one of the exchange's bank accounts.
    477       // Credit account is shared by all incoming transactions
    478       // as per the nature of the request.
    479       debit_account: string;
    480     }
    481 
    482   .. ts:def:: OutgoingBankTransaction
    483 
    484     interface OutgoingBankTransaction {
    485       // Opaque identifier of the returned record.
    486       row_id: SafeUint64;
    487 
    488       // Date of the transaction.
    489       date: Timestamp;
    490 
    491       // Amount transferred.
    492       amount: Amount;
    493 
    494       // Fee paid by the debtor.
    495       // If not null, debtor actually paid amount + debit_fee
    496       // @since **v3**
    497       debit_fee?: Amount;
    498 
    499       // Full payto URI to identify the receiver of funds.
    500       credit_account: string;
    501 
    502       // The wire transfer ID in the outgoing transaction.
    503       wtid: ShortHashCode;
    504 
    505       // Base URL of the exchange.
    506       exchange_base_url: string;
    507     }
    508 
    509 
    510 -----------------
    511 Wire Account APIs
    512 -----------------
    513 
    514 .. http:get:: /account/check
    515 
    516   Check account existence.
    517 
    518   Since protocol **v4**.
    519 
    520   **Request:**
    521 
    522   :query account:
    523     Payto URI of the account.
    524 
    525   **Response:**
    526 
    527   :http:statuscode:`200 OK`:
    528     JSON object of type `AccountInfo`.
    529   :http:statuscode:`400 Bad request`:
    530     Request malformed. The bank replies with an `ErrorDetail` object.
    531   :http:statuscode:`401 Unauthorized`:
    532     Authentication failed, likely the credentials are wrong.
    533   :http:statuscode:`404 Not found`:
    534     * ``TALER_EC_BANK_UNKNOWN_ACCOUNT``: unknown account.
    535   :http:statuscode:`501 Not Implemented`:
    536     This server does not support account check.
    537 
    538   **Details:**
    539 
    540   .. ts:def:: AccountInfo
    541 
    542     interface AccountInfo {
    543     }
    544 
    545 -----------------------
    546 Wire Transfer Test APIs
    547 -----------------------
    548 
    549 Endpoints in this section are only used for integration tests and never
    550 exposed by bank gateways in production.
    551 
    552 .. _twg-admin-add-incoming:
    553 
    554 .. http:post:: /admin/add-incoming
    555 
    556   Simulate a transfer from a customer to the exchange.  This API is *not*
    557   idempotent since it's only used in testing.
    558 
    559   **Request:**
    560 
    561   .. ts:def:: AddIncomingRequest
    562 
    563     interface AddIncomingRequest {
    564       // Amount to transfer.
    565       amount: Amount;
    566 
    567       // Reserve public key that is included in the wire transfer details
    568       // to identify the reserve that is being topped up.
    569       reserve_pub: EddsaPublicKey;
    570 
    571       // Account (as full payto URI) that makes the wire transfer to the exchange.
    572       // Usually this account must be created by the test harness before this
    573       // API is used. An exception is the "fakebank", where any debit account
    574       // can be specified, as it is automatically created.
    575       debit_account: string;
    576     }
    577 
    578   **Response:**
    579 
    580   :http:statuscode:`200 OK`:
    581     The request has been correctly handled, so the funds have been transferred to
    582     the recipient's account.  The body is a `AddIncomingResponse`.
    583   :http:statuscode:`400 Bad request`:
    584     The request is malformed. The bank replies with an `ErrorDetail` object.
    585   :http:statuscode:`401 Unauthorized`:
    586     Authentication failed, likely the credentials are wrong.
    587   :http:statuscode:`404 Not found`:
    588     The endpoint is wrong or the user name is unknown. The bank replies with an `ErrorDetail` object.
    589   :http:statuscode:`409 Conflict`:
    590     The 'reserve_pub' argument was used previously in another transfer, and the specification mandates that reserve public keys must not be reused.
    591 
    592   **Details:**
    593 
    594   .. ts:def:: AddIncomingResponse
    595 
    596     interface AddIncomingResponse {
    597       // Timestamp that indicates when the wire transfer will be executed.
    598       // In cases where the wire transfer gateway is unable to know when
    599       // the wire transfer will be executed, the time at which the request
    600       // has been received and stored will be returned.
    601       // The purpose of this field is for debugging (humans trying to find
    602       // the transaction) as well as for taxation (determining which
    603       // time period a transaction belongs to).
    604       timestamp: Timestamp;
    605 
    606       // Opaque ID of the wire transfer initiation performed by the bank.
    607       // It is different from the /history endpoints row_id.
    608       row_id: SafeUint64;
    609     }
    610 
    611 
    612 
    613 .. _twg-admin-add-kycauth:
    614 
    615 .. http:post:: /admin/add-kycauth
    616 
    617   Simulate a transfer from a customer to the exchange.  This API is *not*
    618   idempotent since it's only used in testing.
    619 
    620   **Request:**
    621 
    622   .. ts:def:: AddKycauthRequest
    623 
    624     interface AddKycauthRequest {
    625       // Amount to transfer.
    626       amount: Amount;
    627 
    628       // Account public key that is included in the wire transfer details
    629       // to associate this key with the originating bank account.
    630       account_pub: EddsaPublicKey;
    631 
    632       // Account (as full payto URI) that makes the wire transfer to the exchange.
    633       // Usually this account must be created by the test harness before this
    634       // API is used. An exception is the "fakebank", where any debit account
    635       // can be specified, as it is automatically created.
    636       debit_account: string;
    637     }
    638 
    639   **Response:**
    640 
    641   :http:statuscode:`200 OK`:
    642     The request has been correctly handled, so the funds have been transferred to
    643     the recipient's account.  The body is a `AddKycauthResponse`.
    644   :http:statuscode:`400 Bad request`:
    645     The request is malformed. The bank replies with an `ErrorDetail` object.
    646   :http:statuscode:`401 Unauthorized`:
    647     Authentication failed, likely the credentials are wrong.
    648   :http:statuscode:`404 Not found`:
    649     The endpoint is wrong or the user name is unknown. The bank replies with an `ErrorDetail` object.
    650 
    651   **Details:**
    652 
    653   .. ts:def:: AddKycauthResponse
    654 
    655     interface AddKycauthResponse {
    656       // Timestamp that indicates when the wire transfer will be executed.
    657       // In cases where the wire transfer gateway is unable to know when
    658       // the wire transfer will be executed, the time at which the request
    659       // has been received and stored will be returned.
    660       // The purpose of this field is for debugging (humans trying to find
    661       // the transaction) as well as for taxation (determining which
    662       // time period a transaction belongs to).
    663       timestamp: Timestamp;
    664 
    665       // Opaque ID of the wire transfer initiation performed by the bank.
    666       // It is different from the /history endpoints row_id.
    667       row_id: SafeUint64;
    668     }
    669 
    670 
    671 Security Considerations
    672 =======================
    673 
    674 For implementors:
    675 
    676 * The withdrawal operation ID must contain enough entropy to be unguessable.
    677 
    678 Design:
    679 
    680 * The user must complete the 2FA step of the withdrawal in the context of their banking
    681   app or online banking Website.
    682   We explicitly reject any design where the user would have to enter a confirmation code
    683   they get from their bank in the context of the wallet, as this would teach and normalize
    684   bad security habits.