Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4374044
feat: add grant spent amount table (#3389)
BlairCurrey Apr 10, 2025
f9ad11f
Merge branch 'main' into bc/raf-1031/grant-spent-amounts
BlairCurrey Apr 24, 2025
06e3acf
Merge branch 'main' into bc/raf-1031/grant-spent-amounts
BlairCurrey May 15, 2025
7a522db
feat: calculate grant spent amounts from new table (#3412)
BlairCurrey Aug 19, 2025
1be38b5
feat: handle grant spent amount calculation on payment completion (#3…
BlairCurrey Nov 1, 2025
eacf1c5
Merge branch 'main' into bc/raf-1031/grant-spent-amounts
BlairCurrey Nov 21, 2025
bb46de4
fix: test to use tenantid
BlairCurrey Nov 21, 2025
eed6d10
fix: update lifecycle tests for mt
BlairCurrey Nov 24, 2025
23756ed
feat: use new open payments spec version
BlairCurrey Nov 24, 2025
e637544
fix: failing lifecycle tests
BlairCurrey Nov 24, 2025
a922f95
feat: add /GET outgoing-payment-grant route (#3756)
BlairCurrey Dec 11, 2025
6608da7
Merge branch 'main' into bc/raf-1031/grant-spent-amounts
BlairCurrey Dec 11, 2025
1371fb9
fix: import
BlairCurrey Dec 11, 2025
c94cf27
chore: fix typo in bruno folder
BlairCurrey Dec 13, 2025
a7c3b38
fix: calculate legacy path in get spent amounts
BlairCurrey Dec 13, 2025
ddbe537
refactor: simplifiy get grant spent amounts logic
BlairCurrey Dec 13, 2025
465e92b
fix: migrate legacy payments
BlairCurrey Dec 13, 2025
bf27169
fix: use trx
BlairCurrey Dec 13, 2025
0214779
test(backend): ensure middleware next is called
BlairCurrey Jan 6, 2026
1b8d896
Merge branch 'main' into bc/raf-1031/grant-spent-amounts
BlairCurrey Jan 6, 2026
457478b
fix(backend): fix potential precision loss
BlairCurrey Jan 12, 2026
99d5ed9
feat: add metric to unexpected state (that doesnt error)
BlairCurrey Jan 12, 2026
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
meta {
name: Vailidating Wallet Address Ownership with Open Payments
name: Validating Wallet Address Ownership with Open Payments
seq: 6
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
meta {
name: Get Outgoing Payment Grant
type: http
seq: 1
}

get {
url: {{senderOpenPaymentsHost}}/outgoing-payment-grant
body: json
auth: none
}

headers {
Authorization: GNAP {{accessToken}}
}

script:pre-request {
const scripts = require('./scripts');

scripts.addHostHeader();

await scripts.addSignatureHeaders();
}

tests {
test("Status code is 201", function() {
expect(res.getStatus()).to.equal(200);
});
}

docs {
Uses GNAP access token to determine the grant.
}
21 changes: 21 additions & 0 deletions packages/auth/src/access/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,27 @@ describe('Access utilities', (): void => {
).toBe(false)
})

test('access comparison does not fail if no request identifier', async (): Promise<void> => {
const grantAccessItemSuperAction = await Access.query(trx).insertAndFetch({
grantId: grant.id,
type: AccessType.IncomingPayment,
actions: [AccessAction.ReadAll],
identifier
})

const requestAccessItem: AccessItem = {
type: 'incoming-payment',
actions: [AccessAction.ReadAll]
}

expect(
compareRequestAndGrantAccessItems(
requestAccessItem,
toOpenPaymentsAccess(grantAccessItemSuperAction)
)
).toBe(true)
})

test('access comparison fails if type mismatch', async (): Promise<void> => {
const grantAccessItemSuperAction = await Access.query(trx).insertAndFetch({
grantId: grant.id,
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/access/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function compareRequestAndGrantAccessItems(

if (
grantAccessIdentifier &&
requestAccessIdentifier &&
requestAccessIdentifier !== grantAccessIdentifier
) {
return false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema
.createTable('outgoingPaymentGrantSpentAmounts', function (table) {
table.uuid('id').notNullable().primary()
table.string('grantId').notNullable()
table.foreign('grantId').references('outgoingPaymentGrants.id')
table.uuid('outgoingPaymentId').notNullable()
table.foreign('outgoingPaymentId').references('outgoingPayments.id')
table.integer('receiveAmountScale').notNullable()
table.string('receiveAmountCode').notNullable()
table.bigInteger('paymentReceiveAmountValue').notNullable()
table.bigInteger('intervalReceiveAmountValue').nullable()
table.bigInteger('grantTotalReceiveAmountValue').notNullable()
table.integer('debitAmountScale').notNullable()
table.string('debitAmountCode').notNullable()
table.bigInteger('paymentDebitAmountValue').notNullable()
table.bigInteger('intervalDebitAmountValue').nullable()
table.bigInteger('grantTotalDebitAmountValue').notNullable()
table.string('paymentState').notNullable()
table.timestamp('intervalStart').nullable()
table.timestamp('intervalEnd').nullable()
table.timestamp('createdAt').defaultTo(knex.fn.now())
})
.then(() => {
return knex.raw(
'CREATE INDEX outgoingPaymentGrantSpentAmounts_grantId_createdAt_desc_idx ON "outgoingPaymentGrantSpentAmounts" ("grantId", "createdAt" DESC)'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll leave it for now.... but I was thinking about changing this:

'CREATE INDEX outgoingPaymentGrantSpentAmounts_grantId_createdAt_desc_idx ON "outgoingPaymentGrantSpentAmounts" ("grantId", "createdAt" DESC)'

To include the interval fields:

knex.raw('CREATE INDEX ... ON "outgoingPaymentGrantSpentAmounts" ("grantId", "intervalStart", "intervalEnd", "createdAt" DESC)')`

because we look it up like this in the worker when processing the payment:

await OutgoingPaymentGrantSpentAmounts.query(deps.knex)
          .where('grantId', grantId)
          .where('intervalStart', latestPaymentSpentAmounts.intervalStart)
          .where('intervalEnd', latestPaymentSpentAmounts.intervalEnd)
          .orderBy('createdAt', 'desc')
          .first()

Dont have great intuition on tradeoffs. I guess it would slow writes some amount in all cases and speed up reads some amount in a subset of cases (when we lookup by interval - only in worker I think). Probably no db storage space difference since a created_at index is going to already lead to a unique index entry per row. Overall I guess it feels like a bad idea. I guess we probably read a bit more (read and write on create, read and write in worker, read in GET /outgoing-payment-grant) but we only lookup by interval in one place.

)
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('outgoingPaymentGrantSpentAmounts')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.alterTable('outgoingPaymentGrants', function (table) {
table.string('interval').nullable()
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('outgoingPaymentGrants', function (table) {
table.dropColumn('interval')
})
}
20 changes: 17 additions & 3 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ import {
httpsigMiddleware,
Grant,
RequestAction,
authenticatedStatusMiddleware
authenticatedStatusMiddleware,
createOutgoingPaymentGrantTokenIntrospectionMiddleware
} from './open_payments/auth/middleware'
import { RatesService } from './rates/service'
import { createSpspMiddleware } from './payment-method/ilp/spsp/middleware'
Expand Down Expand Up @@ -134,13 +135,16 @@ export type AppRequest<ParamsT extends string = string> = Omit<
params: Record<ParamsT, string>
}

export interface WalletAddressUrlContext extends AppContext {
walletAddressUrl: string
export interface IntrospectionContext extends AppContext {
grant?: Grant
client?: string
accessAction?: AccessAction
}

export interface WalletAddressUrlContext extends IntrospectionContext {
walletAddressUrl: string
}

export interface WalletAddressContext extends WalletAddressUrlContext {
walletAddress: WalletAddress
}
Expand Down Expand Up @@ -715,6 +719,16 @@ export class App {
outgoingPaymentRoutes.get
)

// GET /outgoing-payment-grant
// Get grant spent amounts (scoped to interval, if any) from grant
// with outgoing payment create access
router.get(
'/:tenantId/outgoing-payment-grant',
// Expects token used for outgoing payment payment creation
createOutgoingPaymentGrantTokenIntrospectionMiddleware(),
outgoingPaymentRoutes.getGrantSpentAmounts
)

// GET /quotes/{id}
// Read quote
router.get<DefaultState, SignedSubresourceContext>(
Expand Down
Loading
Loading