Skip to content

Commit b6e43a1

Browse files
feat: grant spent amounts (#3757)
* feat: add grant spent amount table (#3389) * feat: add grant spent amount table * fix: change interval start/end string to timestamp * feat: fix filename, update op grant table * fix: rm erroneously added migration to tsconfig * feat: calculate grant spent amounts from new table (#3412) * feat: calculate grant spent amounts from new table * chore: linting * fix: revise grant spent amounts calc to track interval * fix: throw better error, dont inti interval amounts unless they exist * fix: linter, wip commented out code * refactor(backend): rename interval statuses * fix: ensure correct amount (interval/total) is used for grant spent amount * test(backend): grant spent amounts * fix(backend): make logic more typesafe, avoid having to defend w/ internal server error * test(backend): legacy grant calculations * test(backend): classify payment interval fn * test(backend): rm commented-out classify payment interval case * refactor(backend): improve semantics of interval helper fn * test(backend): add non-interval cases to create op * Apply suggestions from code review Co-authored-by: Max Kurapov <max@interledger.org> * refactor: simplifiy interval calc * chore: rm unused imports * refactor: validate grant payment logic * refactor: improve grant spent amount calc type safety * refactor: improve grant spent amount calc type safety * fix: missing trx * chore: handle bad interval state * chore: rm some superfluous comments * chore: format --------- Co-authored-by: Max Kurapov <max@interledger.org> * feat: handle grant spent amount calculation on payment completion (#3674) * feat: calculate grant spent amounts from new table * chore: linting * fix: revise grant spent amounts calc to track interval * fix: throw better error, dont inti interval amounts unless they exist * fix: linter, wip commented out code * refactor(backend): rename interval statuses * fix: ensure correct amount (interval/total) is used for grant spent amount * test(backend): grant spent amounts * fix(backend): make logic more typesafe, avoid having to defend w/ internal server error * test(backend): legacy grant calculations * test(backend): classify payment interval fn * test(backend): rm commented-out classify payment interval case * refactor(backend): improve semantics of interval helper fn * test(backend): add non-interval cases to create op * Apply suggestions from code review Co-authored-by: Max Kurapov <max@interledger.org> * feat: partially handled grant spent amount scenarios on settle * feat: handle non-interval partial settleamount cases * test(backend): failure grant spent amount case for sucessive payment * test(backend): some grant calc interval cases * test(backend): counting grant payments across interval boundaries * chore(backend): rm commented out test * test(backend): new grant spent amount on payment completion * test(backend): improve interval grant calc test to show summation * test(backend): failure edge case * chore(backend): rm comment * chore(backend): fix lint errors * chore(backend): rm debug logs * test(backend): fix failing * Update packages/backend/src/open_payments/payment/outgoing/lifecycle.ts Co-authored-by: Max Kurapov <max@interledger.org> * fix(backend): rm extra spent amount record check * feat(backend): return debit amount from .pay * test(backend): failing test for grant spent amount bug - if 2 payments are create then 2 payments are processed, it assocaites the wrong spent amount with the wrong payment * fix(backend): grant spent amounts calc race conditions - partially implemented fix (missing interval stuff), not fully validated by tests yet * test(backend): grant calc race condition * fix(backend): grant spent amount race conditions with failure * chore(backend): format * test(backend): add failing interval race condition test * fix(backend): interval boundary/race condition edge case - edge case is when there are create/complete race conditions around interval boundaries * test(backend): improve edge case test - ensures we are testing that the latest interval amount is used as the base for interval amounts, not just the payment in the interval being completed * fix(backend): dont query for latest interval payment unecunnecessarily - not necessary if we are within the interval of the payment being completed * fix(backend): type mismatch * test(backend): payment retries do not add additional spent record * fix(backend): use correct debit amount for spent amount recalc * fix(backend): set correct payment state on grant spent amount updates * refactor(backend): dedupe revert/handle grant spent amounts - turned duplicated logic into shared functions - example fn where revert/update is all in one, but felt it was more complicated * chore: rm unused fn * test(backend): fix ilp pay return expectations * refactor(backend): move revert/update grant spent amounts to op service * chore: rm unused import * fix: handle grant spent amounts on payment cancellation * chore: cleanup commented out code * chore: rm submodule added in error * chore(backend): add error logs * fix(backend): explicit grant spent amount formation, more test assertions * refactor(backend): only return receive amt from .pay * fix(backend): build error --------- Co-authored-by: Max Kurapov <max@interledger.org> * fix: test to use tenantid * fix: update lifecycle tests for mt * feat: use new open payments spec version * fix: failing lifecycle tests * feat: add /GET outgoing-payment-grant route (#3756) * feat: add /GET outgoing-payment-grant route * fix: change null state return to return null * fix: rm old comments * fix: add get grant spent amount service tests * fix: add get grant spent amount controller tests * fix(backend): linter * fix(backend): tenanted path * fix: dont require identifier in outgoing payment access item introspection * feat(backend): rm unecessary check * test(auth): add test * test(backend): add middleware test * refactor(backend): cleanup grant spent amount retrieval * chore: fix lint * test(backend): fix grant id check * test(backend): simplify logic, return 0 amount when able * refactor(backend): token introspection middleware - make 2nd introspection middleware for new route instead of generalizing exisitng middleware * test(backend): rm no longer needed test * fix: rm unused import * Update packages/backend/src/open_payments/auth/middleware.ts Co-authored-by: Max Kurapov <max@interledger.org> * fix: return 403 for non create grant for spent amounts used to return null spent amounts * fix: add logs, use luxon interval method * fix: scope of else block * Update packages/backend/src/open_payments/auth/middleware.ts Co-authored-by: Max Kurapov <max@interledger.org> * chore: fix name * feat: test for new middleware/shared fn * chore: make action required --------- Co-authored-by: Max Kurapov <max@interledger.org> * fix: import * chore: fix typo in bruno folder * fix: calculate legacy path in get spent amounts * refactor: simplifiy get grant spent amounts logic * fix: migrate legacy payments * fix: use trx * test(backend): ensure middleware next is called * fix(backend): fix potential precision loss * feat: add metric to unexpected state (that doesnt error) * ci: debug * ci: revert debug --------- Co-authored-by: Max Kurapov <max@interledger.org>
1 parent 9ab21d3 commit b6e43a1

24 files changed

Lines changed: 5125 additions & 225 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
meta {
2-
name: Vailidating Wallet Address Ownership with Open Payments
2+
name: Validating Wallet Address Ownership with Open Payments
33
seq: 6
44
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
meta {
2+
name: Get Outgoing Payment Grant
3+
type: http
4+
seq: 1
5+
}
6+
7+
get {
8+
url: {{senderOpenPaymentsHost}}/outgoing-payment-grant
9+
body: json
10+
auth: none
11+
}
12+
13+
headers {
14+
Authorization: GNAP {{accessToken}}
15+
}
16+
17+
script:pre-request {
18+
const scripts = require('./scripts');
19+
20+
scripts.addHostHeader();
21+
22+
await scripts.addSignatureHeaders();
23+
}
24+
25+
tests {
26+
test("Status code is 201", function() {
27+
expect(res.getStatus()).to.equal(200);
28+
});
29+
}
30+
31+
docs {
32+
Uses GNAP access token to determine the grant.
33+
}

packages/auth/src/access/utils.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,27 @@ describe('Access utilities', (): void => {
296296
).toBe(false)
297297
})
298298

299+
test('access comparison does not fail if no request identifier', async (): Promise<void> => {
300+
const grantAccessItemSuperAction = await Access.query(trx).insertAndFetch({
301+
grantId: grant.id,
302+
type: AccessType.IncomingPayment,
303+
actions: [AccessAction.ReadAll],
304+
identifier
305+
})
306+
307+
const requestAccessItem: AccessItem = {
308+
type: 'incoming-payment',
309+
actions: [AccessAction.ReadAll]
310+
}
311+
312+
expect(
313+
compareRequestAndGrantAccessItems(
314+
requestAccessItem,
315+
toOpenPaymentsAccess(grantAccessItemSuperAction)
316+
)
317+
).toBe(true)
318+
})
319+
299320
test('access comparison fails if type mismatch', async (): Promise<void> => {
300321
const grantAccessItemSuperAction = await Access.query(trx).insertAndFetch({
301322
grantId: grant.id,

packages/auth/src/access/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function compareRequestAndGrantAccessItems(
5757

5858
if (
5959
grantAccessIdentifier &&
60+
requestAccessIdentifier &&
6061
requestAccessIdentifier !== grantAccessIdentifier
6162
) {
6263
return false
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = function (knex) {
6+
return knex.schema
7+
.createTable('outgoingPaymentGrantSpentAmounts', function (table) {
8+
table.uuid('id').notNullable().primary()
9+
table.string('grantId').notNullable()
10+
table.foreign('grantId').references('outgoingPaymentGrants.id')
11+
table.uuid('outgoingPaymentId').notNullable()
12+
table.foreign('outgoingPaymentId').references('outgoingPayments.id')
13+
table.integer('receiveAmountScale').notNullable()
14+
table.string('receiveAmountCode').notNullable()
15+
table.bigInteger('paymentReceiveAmountValue').notNullable()
16+
table.bigInteger('intervalReceiveAmountValue').nullable()
17+
table.bigInteger('grantTotalReceiveAmountValue').notNullable()
18+
table.integer('debitAmountScale').notNullable()
19+
table.string('debitAmountCode').notNullable()
20+
table.bigInteger('paymentDebitAmountValue').notNullable()
21+
table.bigInteger('intervalDebitAmountValue').nullable()
22+
table.bigInteger('grantTotalDebitAmountValue').notNullable()
23+
table.string('paymentState').notNullable()
24+
table.timestamp('intervalStart').nullable()
25+
table.timestamp('intervalEnd').nullable()
26+
table.timestamp('createdAt').defaultTo(knex.fn.now())
27+
})
28+
.then(() => {
29+
return knex.raw(
30+
'CREATE INDEX outgoingPaymentGrantSpentAmounts_grantId_createdAt_desc_idx ON "outgoingPaymentGrantSpentAmounts" ("grantId", "createdAt" DESC)'
31+
)
32+
})
33+
}
34+
35+
/**
36+
* @param { import("knex").Knex } knex
37+
* @returns { Promise<void> }
38+
*/
39+
exports.down = function (knex) {
40+
return knex.schema.dropTableIfExists('outgoingPaymentGrantSpentAmounts')
41+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = function (knex) {
6+
return knex.schema.alterTable('outgoingPaymentGrants', function (table) {
7+
table.string('interval').nullable()
8+
})
9+
}
10+
11+
/**
12+
* @param { import("knex").Knex } knex
13+
* @returns { Promise<void> }
14+
*/
15+
exports.down = function (knex) {
16+
return knex.schema.alterTable('outgoingPaymentGrants', function (table) {
17+
table.dropColumn('interval')
18+
})
19+
}

packages/backend/src/app.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
httpsigMiddleware,
3131
Grant,
3232
RequestAction,
33-
authenticatedStatusMiddleware
33+
authenticatedStatusMiddleware,
34+
createOutgoingPaymentGrantTokenIntrospectionMiddleware
3435
} from './open_payments/auth/middleware'
3536
import { RatesService } from './rates/service'
3637
import { createSpspMiddleware } from './payment-method/ilp/spsp/middleware'
@@ -134,13 +135,16 @@ export type AppRequest<ParamsT extends string = string> = Omit<
134135
params: Record<ParamsT, string>
135136
}
136137

137-
export interface WalletAddressUrlContext extends AppContext {
138-
walletAddressUrl: string
138+
export interface IntrospectionContext extends AppContext {
139139
grant?: Grant
140140
client?: string
141141
accessAction?: AccessAction
142142
}
143143

144+
export interface WalletAddressUrlContext extends IntrospectionContext {
145+
walletAddressUrl: string
146+
}
147+
144148
export interface WalletAddressContext extends WalletAddressUrlContext {
145149
walletAddress: WalletAddress
146150
}
@@ -715,6 +719,16 @@ export class App {
715719
outgoingPaymentRoutes.get
716720
)
717721

722+
// GET /outgoing-payment-grant
723+
// Get grant spent amounts (scoped to interval, if any) from grant
724+
// with outgoing payment create access
725+
router.get(
726+
'/:tenantId/outgoing-payment-grant',
727+
// Expects token used for outgoing payment payment creation
728+
createOutgoingPaymentGrantTokenIntrospectionMiddleware(),
729+
outgoingPaymentRoutes.getGrantSpentAmounts
730+
)
731+
718732
// GET /quotes/{id}
719733
// Read quote
720734
router.get<DefaultState, SignedSubresourceContext>(

0 commit comments

Comments
 (0)