TBD54566975 / tbdex

56 stars 26 forks source link

[http-api] Consider introducing an endpoint that can be used to get a balance #209

Closed mistermoe closed 9 months ago

mistermoe commented 10 months ago

Formally, the concept of an "account" does not exist at the tbdex layer nor is it necessary IMO. The concept of an account however may be beneficial with respect to PFIs. An account needn't be anything complicated. They can be represented as individual DIDs. Technically this already exists because GET /exchanges is effectively returning all exchanges for a given "account" where that account is represented as a DID.

an endpoint like GET /balances could be used to fetch balances that Alice has with a PFI.

For example, If a PFI had a USD -> USDC_STORED_BALANCE offering, Alice would be purchasing USDC to be custodied with the PFI. Alice could then use GET /balances to get her balance.

The PFI could then have another offering like USDC_STORED_BALANCE -> MXN which could be used to off-ramp her stored balance.

This also provides a standarized means to facilitate institutional top-ups

[!NOTE] incorporating something like this would by no means force all PFIs to act as custodians. Rather, it provides the ability for those who choose to provide that functionality.

michaelneale commented 10 months ago

would balances offer something different to what exchanges would if you reconciled them? ie is it sort of like a materialised view of it or is there more functionality?

mistermoe commented 10 months ago

@phoebe-lew and i synced and came up with the following proposal:

At the protocol level, we propose to introduce a specific PaymentMethod kind named STORED_BALANCE. For example:

to purchase USDC that is custodied with the PFI using USD (aka USD -> USDC) the offering would appear as:

[!NOTE] omitted properties for brevity

{
  "payinCurrency": "USD",
  "payoutCurrency": "USDC",
  "payinMethods": [{
    "kind": "DEBIT_CARD"
  }],
  "payoutMethods": [{
    "kind": "STORED_BALANCE"
  }]
}

on the flip side, to utilize a stored balance held with a PFI, an offering would appear as:

{
  "payinCurrency": "USDC",
  "payoutCurrency": "MXN",
  "payinMethods": [{
    "kind": "STORED_BALANCE"
  }],
  "payoutMethods": [{
    "kind": "BANK_ACCT"
  }]
}

Accessing balances from a PFI can be achieved via a secured endpoint:

GET /balances
Authorization: Bearer ${requestToken}

Here, requestToken is created using the same method as employed for retrieving exchanges (aka GET /exchanges).

The response body would be structured as follows:

{
    "balances": [{
        "currency": "USDC",
        "available": "100"
    }]
}

expressed as types:

type GetBalancesResponse = {
  balances: Balance[]
}

type Balance = {
  /** ISO 4217 currency code or widely adopted cryptocurrency code */
  currency: string
  /** same format used to represent currency values across messages */
  available: string
}

this approach supports both institutional top ups in addition to the ability to custody funds for retail customers. For example:

[!NOTE] requiredClaims value has been abbreviated to keep the example terse

a USD -> USDC institutional top-up offering could appear as:

{
  "payinCurrency": "USD",
  "payoutCurrency": "USDC",
  "payinMethods": [{
    "kind": "WIRE_TRANSFER"
  }],
  "payoutMethods": [{
    "kind": "STORED_BALANCE"
  }],
  "requiredClaims": ["KnownBusinessCredential"]
}

a USD -> USDC retail customer offering could appear as:

{
  "payinCurrency": "USD",
  "payoutCurrency": "USDC",
  "payinMethods": [{
    "kind": "WIRE_TRANSFER"
  }],
  "payoutMethods": [{
    "kind": "STORED_BALANCE"
  }],
  "requiredClaims": ["KnownCustomerCredential"]
}

utilizing the stored balance would occur in the same way described in the first example set.

Supporting stored balances is entirely optional. The intent is to provide the ability for PFIs who choose to do so. PFIs that do not support this functionality can respond to GET /balances with a 404: Not Found .

props to @phoebe-lew for coming up with the reserved kind approach

cc: @corcillo

mistermoe commented 10 months ago

would balances offer something different to what exchanges would if you reconciled them? ie is it sort of like a materialised view of it or is there more functionality?

nope you got it! technically no additional functionality needed though likely but not prescribed in any way at the protocol or api spec level

phoebe-lew commented 10 months ago

Further, the rationale for Balances being an array is that there are n-number of currencies that each customer could have balances in. The amount categories (just available for now) are also extensible in case there's a use case for a customer wanting to see their overall pending amount.

corcillo commented 10 months ago

Main question i have, and i probably could think it through on my own but will just state for fun and completeness , what does it look like when an institution that has topped up sends in a message request for one of their customers to do an offramp?

On Tue, Jan 9, 2024 at 10:06 PM Moe Jangda @.***> wrote:

@phoebe-lew https://github.com/phoebe-lew and i synced and came up with the following proposal:

At the protocol level, we propose to introduce a specific PaymentMethod kind named STORED_BALANCE. For example:

to purchase USDC that is custodied with the PFI using USD (aka USD -> USDC) the offering would appear as:

Note

omitted properties for brevity

{ "payinCurrency": "USD", "payoutCurrency": "USDC", "payinMethods": [{ "kind": "DEBIT_CARD" }], "payoutMethods": [{ "kind": "STORED_BALANCE" }] }

on the flip side, to utilize a stored balance held with a PFI, an offering would appear as:

{ "payinCurrency": "USDC", "payoutCurrency": "MXN", "payinMethods": [{ "kind": "STORED_BALANCE" }], "payoutMethods": [{ "kind": "BANK_ACCT" }] }

Accessing balances from a PFI can be achieved via a secured endpoint:

GET /balances Authorization: Bearer ${requestToken}

Here, requestToken is created using the same method as employed for retrieving exchanges (aka GET /exchanges).

The response body would be structured as follows:

{ "balances": [{ "currency": "USDC", "available": "100" }] }

expressed as types:

type GetBalancesResponse = { balances: Balance[]} type Balance = { /* ISO 4217 currency code or widely adopted cryptocurrency code / currency: string /* same format used to represent currency values across messages / available: string}

this approach supports both institutional top ups in addition to the ability to custody funds for retail customers. For example:

Note

requiredClaims value has been abbreviated to keep the example terse

a USD -> USDC institutional top-up offering could appear as:

{ "payinCurrency": "USD", "payoutCurrency": "USDC", "payinMethods": [{ "kind": "WIRE_TRANSFER" }], "payoutMethods": [{ "kind": "STORED_BALANCE" }], "requiredClaims": ["KnownBusinessCredential"] }

a USD -> USDC retail customer offering could appear as:

{ "payinCurrency": "USD", "payoutCurrency": "USDC", "payinMethods": [{ "kind": "WIRE_TRANSFER" }], "payoutMethods": [{ "kind": "STORED_BALANCE" }], "requiredClaims": ["KnownCustomerCredential"] }

utilizing the stored balance would occur in the same way described in the first example set.

Supporting stored balances is entirely optional. The intent is to provide the ability for PFIs who choose to do so. PFIs that do not support this functionality can respond to GET /balances with a 404: Not Found .

props to @phoebe-lew https://github.com/phoebe-lew for coming up with the reserved kind approach

cc: @corcillo https://github.com/corcillo

— Reply to this email directly, view it on GitHub https://github.com/TBD54566975/tbdex/issues/209#issuecomment-1884242994, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABC2ZO2HKM5X6N44YASCKMTYNYVVVAVCNFSM6AAAAABA2HVEW2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQOBUGI2DEOJZGQ . You are receiving this because you were mentioned.Message ID: @.***>

phoebe-lew commented 10 months ago

@corcillo So let's say a DIDpay customer is offramping USDC -> MXN. The offering looks like:

{
  "payinCurrency": "USDC",
  "payoutCurrency": "MXN",
  "payinMethods": [{
    "kind": "STORED_BALANCE"
  }],
  "payoutMethods": [{
    "kind": "SPEI"
  }],
  "requiredClaims": ["SanctionsCredential"]
}

Because it's drawing against DIDpay's stored balance with the PFI. The wallet app can associate the exchange to their own internal transaction id via the external_id field: https://github.com/orgs/TBD54566975/projects/29/views/1?pane=issue&itemId=44614947