Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(backend): remove payment balance accounts #197

Merged
merged 21 commits into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
32cd5fd
chore(backend): send/receive payments from/to settlement account
wilsonianb Dec 2, 2021
7d65fa0
chore(backend): make incoming and outgoing accounts distinct types
wilsonianb Dec 3, 2021
9edf1bb
chore(backend): rename Funding payment state Ready
wilsonianb Dec 3, 2021
55da10b
chore(backend): remove quote amountSent from query resolver
wilsonianb Dec 6, 2021
344014c
chore(backend): add getTotalSent/getTotalReceived
wilsonianb Dec 9, 2021
4fbc595
chore(backend): accounts -> accounting in connector
wilsonianb Dec 9, 2021
65a5b00
chore(backend): remove web monetization service
wilsonianb Dec 9, 2021
9144037
chore(backend): match Open Payments invoice spec
wilsonianb Dec 9, 2021
0578c64
chore(backend): add AccountType.Unrestricted
wilsonianb Dec 9, 2021
cc0000f
chore(backend): add invoice getReceiveLimit
wilsonianb Dec 9, 2021
51266ca
chore(backend): handleFunding -> handleReady
wilsonianb Dec 10, 2021
34052dd
chore(docs): update transaction-api.md
wilsonianb Dec 10, 2021
741924f
chore(backend): invoice expiresAt required
wilsonianb Dec 11, 2021
b3d554e
chore(backend): add send and receive account types
wilsonianb Dec 15, 2021
6b20485
chore(backend): add OutgoingPaymentError
wilsonianb Dec 15, 2021
161dbcc
Merge branch 'main' into bw-rm-payment-account
wilsonianb Dec 18, 2021
808f912
chore(backend): remove send and receive accounts
wilsonianb Dec 18, 2021
c9ae748
feat(backend): add fundOutgoingPayment
wilsonianb Dec 18, 2021
3be8230
chore(backend): deactivate fully paid invoice
wilsonianb Dec 18, 2021
88ba6d1
chore(backend): deactivate paid invoice in transaction
wilsonianb Dec 20, 2021
6478831
chore(backend): deactivate fully paid invoice in single query
wilsonianb Dec 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 12 additions & 17 deletions docs/transaction-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,37 @@ If the payment destination cannot be resolved, no payment is created and the que

### Quoting

To begin a payment attempt, an instance acquires a lock to setup and quote the payment, advancing it from `Quoting` to the `Funding` state.
To begin a payment attempt, an instance acquires a lock to setup and quote the payment, advancing it from `Quoting` to the `Ready` state.

First, the recipient Open Payments account or invoice is resolved. Then, the STREAM sender quotes the payment to probe the exchange rate, compute a minimum rate, and discover the path maximum packet amount.

Quotes can end in 3 states:

1. Success. The STREAM sender successfully established a connection to the recipient, and discovered rates and the path capacity. This advances the state to `Funding`. The parameters of the quote are persisted so they may be resumed if the payment is funded. Rafiki also assigns a deadline based on the expected validity of its slippage parameters for the wallet to fund the payment.
1. Success. The STREAM sender successfully established a connection to the recipient, and discovered rates and the path capacity. This advances the state to `Ready`. The parameters of the quote are persisted so they may be resumed if the payment is funded. Rafiki also assigns a deadline based on the expected validity of its slippage parameters for the wallet to fund the payment.
2. Irrevocable failure. In cases such as if the payment pointer or account URL was semantically invalid, the invoice was already paid, a terminal ILP Reject was encountered, or the rate was insufficient, the payment is unlikely to ever succeed, or requires some manual intervention. These cases advance the state to `Cancelled`.
3. Recoverable failure. In the case of some transient errors, such as if the Open Payments HTTP query failed, the quote couldn't complete within the timeout, or no external exchange rate was available, Rafiki may elect to automatically retry the quote. This returns the state to `Quoting`, but internally tracks that the quote failed and when to schedule another attempt.

After the quote ends and state advances, the lock on the payment is released.

### Authorization

After quoting completes, Rafiki notifies the wallet operator to add `maxSourceAmount` of the quote from the funding wallet account owned by the payer to the payment, reserving the maximum requisite funds for the payment attempt.
After quoting completes, Rafiki notifies the wallet operator via a webhook event. The wallet operator should reserve `maxSourceAmount` of the quote from the funding wallet account owned by the payer, reserving the maximum requisite funds for the payment attempt.

If the payment intent did not specify `autoApprove` of `true`, a client should manually approve the payment, based on the parameters of the quote, before the wallet adds payment liquidity.
If the payment intent did not specify `autoApprove` of `true`, a client should manually approve the payment, based on the parameters of the quote, before the wallet reserves payment liquidity.

This step is necessary so the end user can precisely know the maximum amount of source units that will leave their account. Typically, the payment application will present these parameters in the user interface before the user elects to approve the payment. This step is particularly important for invoices, to prevent an unbounded sum from leaving the user's account. During this step, the user may also be presented with additional information about the payment, such as details of the payment recipient, or how much is expected to be delivered.

Authorization ends in two possible states:

1. Approval. If the user approves the payment before its funding deadline, or `autoApprove` was `true`, the wallet funds the payment and the state advances to `Sending`.
1. Approval. If the user approves the payment before its activation deadline, or `autoApprove` was `true`, the wallet reserves `maxSourceAmount` and calls `sendPayment` to advance the state to `Sending`.

2. Cancellation. If the user explicitly cancels the quote, or the funding deadline is exceeded, the state advances to `Cancelled`. In the latter case, too much time has elapsed for the enforced exchange rate to remain accurate.
2. Cancellation. If the user explicitly cancels the quote, or the activation deadline is exceeded, the state advances to `Cancelled`. In the latter case, too much time has elapsed for the enforced exchange rate to remain accurate.

### Payment execution

An instance acquires a lock on a payment with a `Funding` state and advances it to `Sending`. The STREAM will use the quote parameters acquired during the `Quoting` state.
To send, an instance acquires a lock on a payment with a `Sending` state.

The instance connects to the Interledger account it created via ILP-over-HTTP, and sends the payment with STREAM.
The instance sends the payment with STREAM, which uses the quote parameters acquired during the `Quoting` state.

After the payment completes, the instance releases the lock on the payment and advances the state depending upon the outcome:

Expand All @@ -51,7 +51,7 @@ After the payment completes, the instance releases the lock on the payment and a

3. Recoverable failure. In cases such as an idle timeout, Rafiki may elect to automatically retry the payment. The state remains `Sending`, but internally tracks that the payment failed and when to schedule another attempt.

In the `Completed` and `Cancelled` cases, the wallet is notifed of any remaining funds in the Interledger account. Note: if the payment is retried, the same Interledger account is used for the subsequent attempt.
In the `Completed` and `Cancelled` cases, the wallet is notifed of the total amount sent via a webhook event. The wallet may return unused reserved funds to the payer's wallet account and/or collect a portion as fees.

### Manual recovery

Expand Down Expand Up @@ -92,12 +92,7 @@ The intent must include `invoiceUrl` xor (`paymentPointer` and `amountToSend`).
| `quote.minExchangeRate` | No | `Float` | Aggregate exchange rate the payment is guaranteed to meet, as a ratio of destination base units to source base units. Corresponds to the minimum exchange rate enforced on each packet (_except for the final packet_) to ensure sufficient money gets delivered. For strict bookkeeping, use `maxSourceAmount` instead. |
| `quote.lowExchangeRateEstimate` | No | `Float` | Lower bound of probed exchange rate over the path (inclusive). Ratio of destination base units to source base units. |
| `quote.highExchangeRateEstimate` | No | `Float` | Upper bound of probed exchange rate over the path (exclusive). Ratio of destination base units to source base units. |
| `paymentPointerId` | No | `String` | Account id of the payment pointer owned by the payer. |
| `account` | No | `Object` | |
| `account.id` | No | `ID` | Id of the payment's Interledger account |
| `account.asset` | No | `Object` | |
| `account.asset.scale` | No | `Integer` | |
| `account.asset.code` | No | `String` | |
| `accountId` | No | `String` | Id of the payer's Open Payments account. |
| `destinationAccount` | No | `Object` | |
| `destinationAccount.scale` | No | `Integer` | |
| `destinationAccount.code` | No | `String` | |
Expand All @@ -108,8 +103,8 @@ The intent must include `invoiceUrl` xor (`paymentPointer` and `amountToSend`).

### `PaymentState`

- `QUOTING`: Initial state. In this state, an empty payment account is generated, and the payment is automatically resolved & quoted. On success, transition to `FUNDING`. On failure, transition to `Cancelled`.
- `FUNDING`: Awaiting the wallet to add payment liquidity. If `intent.autoApprove` is not set, the wallet gets user approval before reserving money from the user's wallet account. On success, transition to `Sending`.
- `QUOTING`: Initial state. In this state, an empty payment account is generated, and the payment is automatically resolved & quoted. On success, transition to `READY`. On failure, transition to `Cancelled`.
- `READY`: Awaiting the wallet to reserve funds. If `intent.autoApprove` is not set, the wallet gets user approval before reserving money from the user's wallet account. Calling `sendPayment` transitions to `Sending`. Otherwise, transitions to `Cancelled` when the quote expires.
- `SENDING`: Stream payment from the payment account to the destination.
- `CANCELLED`: The payment failed. (Though some money may have been delivered). Requoting transitions to `Quoting`.
- `COMPLETED`: Successful completion.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ exports.up = function (knex) {
table.foreign('accountId').references('accounts.id')
table.boolean('active').notNullable()
table.string('description').nullable()
table.timestamp('expiresAt').nullable()
table.bigInteger('amountToReceive').nullable()
table.timestamp('expiresAt').notNullable()
table.bigInteger('amount').notNullable()

table.timestamp('createdAt').defaultTo(knex.fn.now())
table.timestamp('updatedAt').defaultTo(knex.fn.now())
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ exports.up = function (knex) {
table.string('state').notNullable().index() // PaymentState
table.string('error').nullable()
table.integer('stateAttempts').notNullable().defaultTo(0)
table.boolean('withdrawLiquidity').notNullable().defaultTo(false).index()

table.string('intentPaymentPointer').nullable()
table.string('intentInvoiceUrl').nullable()
Expand All @@ -26,6 +25,9 @@ exports.up = function (knex) {
table.bigInteger('quoteHighExchangeRateEstimateNumerator').nullable()
table.bigInteger('quoteHighExchangeRateEstimateDenominator').nullable()

// Amount already sent at the time of the quote
table.bigInteger('quoteAmountSent').nullable()

// Open payments account corresponding to wallet account
// from which to request funds for payment
table.uuid('accountId').notNullable()
Expand Down
21 changes: 15 additions & 6 deletions packages/backend/src/accounting/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@ import {
CreateAccountError as CreateAccountErrorCode
} from 'tigerbeetle-node'

import { AccountType, ServiceDependencies } from './service'
import { ServiceDependencies } from './service'
import { CreateAccountError } from './errors'
import { AccountIdOptions, getAccountId } from './utils'

const ACCOUNT_RESERVED = Buffer.alloc(48)

// Credit and debit accounts can both send and receive
// but are restricted by their respective Tigerbeetle flags.
// In Rafiki transfers:
// - the source account's debits increase
// - the destination account's credits increase
export enum AccountType {
Credit = 'Credit', // debits_must_not_exceed_credits
Debit = 'Debit' // credits_must_not_exceed_debits
}

export type CreateAccountOptions = AccountIdOptions & {
asset: {
unit: number
}
type: AccountType
unit: number
debits?: bigint
}

export async function createAccounts(
Expand All @@ -26,13 +35,13 @@ export async function createAccounts(
id: getAccountId(account),
user_data: BigInt(0),
reserved: ACCOUNT_RESERVED,
unit: account.asset.unit,
unit: account.unit,
code: 0,
flags:
account.type === AccountType.Debit
? AccountFlags.credits_must_not_exceed_debits
: AccountFlags.debits_must_not_exceed_credits,
debits_accepted: BigInt(0),
debits_accepted: account.debits || BigInt(0),
debits_reserved: BigInt(0),
credits_accepted: BigInt(0),
credits_reserved: BigInt(0),
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/accounting/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class CommitTransferError extends Error {
export enum TransferError {
AlreadyCommitted = 'AlreadyCommitted',
AlreadyRolledBack = 'AlreadyRolledBack',
DifferentAssets = 'DifferentAssets',
DifferentUnits = 'DifferentUnits',
InsufficientBalance = 'InsufficientBalance',
InsufficientDebitBalance = 'InsufficientDebitBalance',
InsufficientLiquidity = 'InsufficientLiquidity',
Expand All @@ -42,8 +42,8 @@ export enum TransferError {
TransferExists = 'TransferExists',
TransferExpired = 'TransferExpired',
UnknownTransfer = 'UnknownTransfer',
UnknownSourceBalance = 'UnknownSourceBalance',
UnknownDestinationBalance = 'UnknownDestinationBalance'
UnknownSourceAccount = 'UnknownSourceAccount',
UnknownDestinationAccount = 'UnknownDestinationAccount'
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
Expand Down
Loading