-
Notifications
You must be signed in to change notification settings - Fork 77
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
Conversation
Outgoing payments and invoices have accounts for tracking the amounts sent and received, but they do not represent (withdraw-able) balances. Add sendOutgoingPayment mutation resolver. Wallet operators will be notified when quote is complete (TODO) and call sendOutgoingPayment after payment is approved (if necessary) and funds are reserved in the sender's wallet account.
Rename transferFunds to sendAndReceive
accounts.outgoing.id | ||
assert.ok(accounts.outgoing.receivedAccountId) | ||
const receiveLimit = await services.accounts.getBalance( | ||
accounts.outgoing.receivedAccountId |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming of receivedAccountId
confuses me. It isn't the amount that has been received, it is the maximum amount that can be received, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's confusing because it depends:
https://github.com/interledger/rafiki/pull/197/files#diff-e5a9751319994ef97d1413f0c3bc6587f756163958ad21f877a5fe7d84a08661R81-R89
I considered having separate receivedAccountId
and receiveLimitAccountId
, but transferFunds
sendAndReceive
would treat them the same.
@@ -302,6 +304,7 @@ type PaymentQuote { | |||
minExchangeRate: Float! | |||
lowExchangeRateEstimate: Float! | |||
highExchangeRateEstimate: Float! | |||
amountSent: UInt64! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this parameter need to be exposed on the API?
If it does need to be exposed, it should have a more descriptive name to ensure that the caller doesn't assume that it should be subtracted from (e.g.) the maxSourceAmount
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't need to be exposed since it's in the OutgoingPaymentOutcome
@matdehaast Here's the possible webhooks for reference #199 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good work @wilsonianb.
I do still worry that we are losing some accounting principals and accurate tracking of money movement by going in this direction. But I can see that it does simplify things greatly.
My assumption is that this is all to account for the need to enforce limits on certain payment accounts?
I wonder if there is a better abstraction where we can have another primitive within the account service that is called a limit account or something along those lines and we could bind that to an account. I'm trying to draw up some T accounts for this PR to see if we actually lose anything meaningful.
: AccountFlags.debits_must_not_exceed_credits, | ||
: account.type === AccountType.Credit | ||
? AccountFlags.debits_must_not_exceed_credits | ||
: 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does having a zero value here make sense? Should we not just define an account type enum for the case when its neither to remove ambiguity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
A reason I didn't add and expose one is that currently the only non-Debit
/Credit
account is being created within the accounting service (in createAssetAccounts
).
export type AccountOptions = (BasicAccountOptions | AssetAccountOptions) & { | ||
receivedAccountId?: string | ||
sentAccountId?: string | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can an account have both a received and sent account id?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I ask is, I'm wondering if its better to just have a singular account and then use a flag/enum/account type to determine if you are a received or sent account
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this comment is on an outdated commit. Here's the final form:
https://github.com/interledger/rafiki/pull/197/files#diff-72a5530c2c89f105f75e071479a382e8f8408459d37968fb93b546cca4783b31R75-R91
There's currently not a situation in which an account would have both a received and sent account id.
With this previous iteration, it could have been consolidated to one account whose type could be checked, but I like how it fit into separating the connector's incoming and outgoing account types.
https://github.com/interledger/rafiki/pull/197/files#diff-08011ba8622ef9b1a38af778014f9471eb8ce942adc8609074f019765f7338f6R26-R43
// Invoice accounts are credited by the amounts received by the invoice. | ||
// | ||
// If amountToReceive is specified, the invoice account is initially | ||
// debited by the amountToReceive, credits are restricted such that the | ||
// invoice cannot receive more than that amount. The account balance | ||
// represents the remaining receive limit. | ||
// | ||
// Otherwise, the invoice account balance represents the total amount | ||
// received by the invoice. | ||
await deps.accountingService.createAccount({ | ||
id: invoice.id, | ||
asset: invoice.account.asset, | ||
type: AccountType.Credit, | ||
receiveLimit: amountToReceive | ||
type: amountToReceive ? AccountType.Debit : AccountType.Credit | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wilsonianb are you worried this might be a footgun and catch someone else out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
I'd like to separate web monetization from invoices and make the invoice model follow the open payments spec which would make amountToReceive
a required field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wilsonianb ah I see that is a required field. I can't for the life of me remember why. I wonder how fixed send amounts are handled if amount is required...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const amountReceived = invoice.amountToReceive | ||
? invoice.amountToReceive + POSITIVE_SLIPPAGE - balance | ||
: balance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should have a function in the invoice service rather to account for this, if somewhere else wants to quote the invoice balance, they will need to know this has to happen
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started to add a function yesterday and then I added getTotalReceived
to the accountingService
. 2ce7778
I'll put that in this pr.
Add SPSP fallback tigerbeetle account to Open Payments accounts.
We were already effectively enforcing limits on payments. For outgoing payments, there's no longer a liquidity balance to enforce. The amount sent is restricted by Invoices do still enforce the receive limit, but there's similarly no enforcement of an invoice's withdrawable liquidity. It has an amount received that can only go up, which the wallet can reflect in the receiver's wallet account however it wants. |
This won't work as is for an intra-rafiki same asset outgoing payment to invoice. It'll get |
Send and receive accounts have their own tigerbeetle account units, preventing transfers between them or with liquidity accounts (asset liquidity, settlement, and peer accounts). Fix same account error for same asset intra-rafiki transfers.
☝️ that fixes the same account issue. |
|
||
const ASSET_ACCOUNTS_RESERVED = 32 | ||
const ASSET_ACCOUNTS_RESERVED = 8 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this constant for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used in generating deterministic asset account ids:
https://github.com/interledger/rafiki/blob/main/packages/backend/src/accounting/utils.ts#L28-L30
We're now down to two types of asset accounts (liquidity & settlement).
I was actually just considering replacing it with:
const MAX_ASSETS = 2**16
based on Tigerbeetle's two byte unit field. And then getAssetAccountId
could do:
BigInt(type * MAX_ASSETS + unit)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't miscellaneous account uuids get generated into that range?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, which is why I was still favoring ASSET_ACCOUNTS_RESERVED
over MAX_ASSETS
since the range is limited by the number of assets.
However, I think we would safely fail to create an asset if any of its tigerbeetle account ids are already used.
https://github.com/interledger/rafiki/blob/main/packages/backend/src/asset/service.ts#L57-L68
We'd end up with a unit sequence gap, and retrying would attempt to create the asset with the next unit value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also had the thought that validateId
in accountingService.createAccount
could disallow ids in the possible asset id range.
parent, | ||
args, | ||
ctx | ||
): ResolversTypes['OutgoingPaymentResponse'] => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the payment state is not Ready
should that be a client (400) error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, and maybe the same for requoteOutgoingPayment
and cancelOutgoingPayment
.
Should we define a WrongPaymentState
error that the resolvers can catch and return 400
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. The alternative would be to check the state in the resolver itself, but I think that validation logic is better off in the service (where it already is).
Return 400 for unknown or wrong state payments.
After talking with Matt, I'm in agreement that sending from and receiving to the settlement account prevents the wallet operator from getting an accurate snapshot of liabilities.
I'll convert the payment accounts back to normal in this pr and do liquidity deposits/withdrawals and webhooks in separate pr(s). |
Payments have a single liquidity account (no extra limit balances). Balances are no longer adjusted for unfulfillable rate probe packets.
Transition to Sending if funding provides adequate balance. Quoting transitions to Sending if balance is already sufficient. Remove sendOutgoingPayment. Rename Ready state to Funding.
const invoice = await Invoice.query(deps.knex).findById(invoiceId) | ||
if (invoice.amount <= amountReceived) { | ||
await invoice.$query(deps.knex).patch({ | ||
active: false | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it cause problems if this query + patch are in separate transactions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so since the amount currently isn't modified anywhere, but this could be put in a single
await Invoice.query(deps.knex).patch({
active: false
}).where('id', invoiceId).andWhere('amount', '<=', amountReceived)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into objection/knex type errors trying to compare with a bigint in andWhere
so I've put the query and patch in a transaction 88ba6d1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you try passing the toString
of the bigint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope 🙃
6478831
const payment = await OutgoingPayment.query(trx) | ||
.findById(id) | ||
.forUpdate() | ||
.withGraphFetched('account.asset') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you double-check that the forUpdate
doesn't also get carried over the the account.asset
being fetched? (i.e. that the payment row is locked but not the account/asset). I know we do this elsewhere too but it didn't occur to me until now...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vincit/objection.js#1608
I'm guessing it would for withGraphJoined
Changes proposed in this pull request
Send outgoing payments from and receive invoice payments to settlement accounts.Outgoing payments and invoices have accounts for tracking the amountssent and received, but they do not represent (withdraw-able) balances.
sent and received.
sendOutgoingPaymentfundOutgoingPayment
mutation resolver. Wallet operators will benotified when quote is complete (TODO) and call
fundOutgoingPayment
after payment is approved (if necessary) and funds are reserved in
the sender's wallet account.
RenameFunding
payment state asReady
Context
Remove outgoing payment accounts #196Checklist
fixes #number