Skip to content

feat(backend): add middleware that handles STREAM KYC/additional data frames#3706

Merged
sanducb merged 24 commits intofeature/encrypted-data-exchangefrom
bs/raf-1185
Apr 6, 2026
Merged

feat(backend): add middleware that handles STREAM KYC/additional data frames#3706
sanducb merged 24 commits intofeature/encrypted-data-exchangefrom
bs/raf-1185

Conversation

@sanducb
Copy link
Copy Markdown
Contributor

@sanducb sanducb commented Oct 16, 2025

Changes proposed in this pull request

  • This PR introduces support for sending additional data over ILP prepare packets. This allows a receiving ASE to reject/fulfill a payment packet based on the validity of the additional data.

Context

Closes #RAF-1185

Checklist

  • Related issues linked using fixes #number
  • Tests added/updated
  • Make sure that all checks pass
  • Bruno collection updated (if necessary)
  • Documentation issue created with user-docs label (if necessary)
  • OpenAPI specs updated (if necessary)

@netlify
Copy link
Copy Markdown

netlify Bot commented Oct 16, 2025

Deploy Preview for brilliant-pasca-3e80ec failed. Why did it fail? →

Name Link
🔨 Latest commit 8b77d8d
🔍 Latest deploy log https://app.netlify.com/projects/brilliant-pasca-3e80ec/deploys/69b011bf4024f50008dec8b6

@github-actions github-actions Bot added pkg: backend Changes in the backend package. type: source Changes business logic labels Oct 16, 2025
Comment thread packages/backend/src/payment-method/ilp/connector/core/middleware/kyc-decision.ts Outdated
Copy link
Copy Markdown
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Only focused on the kyc decision middleware here


const readDecision = async (): Promise<string | undefined> => {
try {
const value = await redis.get(cacheKey)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We have a poll utility function for this in utils.ts


const incomingPaymentId = ctx.state.streamDestination
const connectionId = ctx.state.connectionId ?? 'unknown'
const cacheKey = `kyc_decision:${incomingPaymentId}:${connectionId}`
Copy link
Copy Markdown
Contributor

@mkurapov mkurapov Jan 19, 2026

Choose a reason for hiding this comment

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

I think we should incorporate the polling inside the existing incoming payment service that will handle the logic for this. That is where we can also put the code to actually confirm or reject the partial incoming payment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the incoming payment service is fine for this one.

}

const incomingPaymentId = ctx.state.streamDestination
const connectionId = ctx.state.connectionId ?? 'unknown'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's generate our own UUID partialIncomingPaymentId

import { ILPContext, ILPMiddleware } from '../rafiki'
import { StreamState } from './stream-address'

export function createKycDecisionMiddleware(): ILPMiddleware {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should not have any references to "kyc" in this code change, but simply partialPaymentDecisionMiddleware or something

Copy link
Copy Markdown
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Few comments

createIncomingErrorHandlerMiddleware(ilpAddress),
createStreamAddressMiddleware(),

createKycDecisionMiddleware(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we move this to be after the createBalanceMiddleware ? That way, we will only publish the webhook & start polling for the decision after we were able to created a pending transfer between the peer account to the incoming payment account, confirming we have enough funds to make the transfer anyway.

dataToTransmit?: string
): Promise<IncomingPayment | IncomingPaymentError> {
const { config, knex } = deps
options?: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
options?: {
options: {

(since we would only call this function when it is defined)

request: async (): Promise<string> => {
try {
const value = await redis.get(cacheKey)
return value ?? ''
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should expect the value to be stringified { approved: boolean, reason?: string }), and we can then just check for approved boolean (see #3687)

createIncomingErrorHandlerMiddleware(ilpAddress),
createStreamAddressMiddleware(),

createPartialPaymentDecisionMiddleware(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we move this to be after the createBalanceMiddleware ? That way, we will only publish the webhook & start polling for the decision after we were able to created a pending transfer between the peer account to the incoming payment account, confirming we have enough funds to make the transfer anyway.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thoughts on this ^?

plugin,
destination,
quote,
appData: Buffer.from('hello kyc')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can use the outgoing payments' dataToTransmit here now I think

plugin,
destination,
quote,
appData: Buffer.from('hello kyc')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can use the outgoing payments' dataToTransmit here now I think

Copy link
Copy Markdown
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Few comments

Copy link
Copy Markdown
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Few comments

@sanducb sanducb changed the base branch from main to feature/encrypted-data-exchange March 10, 2026 12:43
@github-actions github-actions Bot removed the pkg: frontend Changes in the frontend package. label Mar 24, 2026
@sanducb sanducb requested a review from mkurapov March 24, 2026 14:19
@sanducb sanducb marked this pull request as ready for review March 24, 2026 14:20
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 24, 2026

🚀 Performance Test Results

Test Configuration:

  • VUs: 4
  • Duration: 1m0s

Test Metrics:

  • Requests/s: 40.69
  • Iterations/s: 13.59
  • Failed Requests: 0.00% (0 of 2449)
📜 Logs

> performance@1.0.0 run-tests:testenv /home/runner/work/rafiki/rafiki/test/performance
> ./scripts/run-tests.sh -e test "-k" "-q" "--vus" "4" "--duration" "1m"

Cloud Nine GraphQL API is up: http://localhost:3101/graphql
Cloud Nine Wallet Address is up: http://localhost:3100/
Happy Life Bank Address is up: http://localhost:4100/
cloud-nine-wallet-test-backend already set
cloud-nine-wallet-test-auth already set
happy-life-bank-test-backend already set
happy-life-bank-test-auth already set
     data_received..................: 884 kB 15 kB/s
     data_sent......................: 1.9 MB 31 kB/s
     http_req_blocked...............: avg=7.37µs   min=2.42µs   med=5.31µs   max=2.46ms   p(90)=6.43µs   p(95)=6.97µs  
     http_req_connecting............: avg=417ns    min=0s       med=0s       max=414.68µs p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=97.65ms  min=7.31ms   med=77.8ms   max=634.29ms p(90)=173.04ms p(95)=194.64ms
       { expected_response:true }...: avg=97.65ms  min=7.31ms   med=77.8ms   max=634.29ms p(90)=173.04ms p(95)=194.64ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 2449
     http_req_receiving.............: avg=92.97µs  min=26.91µs  med=81.75µs  max=3.17ms   p(90)=119.8µs  p(95)=150.59µs
     http_req_sending...............: avg=35.35µs  min=11.04µs  med=28.35µs  max=1.38ms   p(90)=41.21µs  p(95)=53.29µs 
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=97.52ms  min=7.14ms   med=77.68ms  max=634.11ms p(90)=172.88ms p(95)=194.54ms
     http_reqs......................: 2449   40.6944/s
     iteration_duration.............: avg=293.98ms min=173.88ms med=284.32ms max=1.11s    p(90)=350.45ms p(95)=377.14ms
     iterations.....................: 818    13.592495/s
     vus............................: 4      min=4       max=4 
     vus_max........................: 4      min=4       max=4 

Comment thread package.json Outdated
Comment on lines +80 to +81
"@interledger/stream-receiver": "0.3.3-alpha.4",
"@interledger/pay": "0.4.0-alpha.10",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's put these just in the backend package (unless I'm mistaken and they are used elsewhere too)

destination,
quote,
appData: outgoingPayment.dataToTransmit
? Buffer.from(outgoingPayment.dataToTransmit, 'utf8')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if you use outgoingPayment.getDataToTransmit() it will be decrypted, such that the reciever won't have to do that. It is already encrypted via STREAM

createIncomingErrorHandlerMiddleware(ilpAddress),
createStreamAddressMiddleware(),

createPartialPaymentDecisionMiddleware(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thoughts on this ^?

message ?? 'Error processing partial payment',
'utf8'
)
ctx.response.reply = replyOrMoney.finalDecline(errorData)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What are the consequences here if we do not throw (like before)?

Copy link
Copy Markdown
Contributor Author

@sanducb sanducb Mar 26, 2026

Choose a reason for hiding this comment

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

When setting the reply by calling finalDecline we respond to the sender with a final F99 error (along with the error message coming from the ASE/Rafiki) and closing frame on the reject. Throwing was not correct IMO since purposefully rejecting a payment shouldn't be treated as an unexpected error i.e. handled by the error middleware.

)
return false
}
const cacheKey = `${PARTIAL_PAYMENT_DECISION_PREFIX}:${options.id}:${options.partialPaymentId}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe let's have a separate small function for this so it's consistent in both methods

'SEND_TENANT_WEBHOOKS_TO_OPERATOR',
false
),
partialPaymentDecisionMaxWaitMs: envInt(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe we should have the decision behaviour (send webhook & poll) false by default (even if there are additional data frames on the packet). What do you think? The ASE could enable it smoothly.

@github-actions github-actions Bot added the pkg: documentation Changes in the documentation package. label Mar 26, 2026
@sanducb sanducb requested a review from mkurapov March 27, 2026 14:59
| `ENABLE_AUTO_PEERING` | `backend.enable.autoPeering` | `false` | When `true`, auto-peering is enabled. |
| `ENABLE_MANUAL_MIGRATIONS` | `backend.enableManualMigrations` | `false` | When `true`, you must run the database manually with the command `npm run knex – migrate:latest –env production` |
| `ENABLE_SPSP_PAYMENT_POINTERS` | `backend.enable.spspPaymentPointers` | `true` | When `true`, the SPSP route is enabled. |
| `DB_ENCRYPTION_SECRET` | _undefined_ | _undefined_ | Base64-encoded 32-byte secret used to encrypt/decrypt transmitted payment data (`dataToTransmit`) stored on incoming/outgoing payment records and events. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We don't need to include it in the v1-beta docs

Comment on lines +221 to +234
const { cipherText, tag, iv } = JSON.parse(rawDataToTransmit) as {
cipherText: string
tag: string
iv: string
}

const decipher = createDecipheriv(
'aes-256-gcm',
Uint8Array.from(Buffer.from(dbEncryptionSecret, 'base64')),
iv
)
decipher.setAuthTag(Uint8Array.from(Buffer.from(tag, 'base64')))
decipher.update(cipherText, 'base64', 'utf8')
decipher.final('utf8')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you think we should decrypt this data before publishing the webhook?

walletAddressService: WalletAddressService
assetService: AssetService
config: IAppConfig
redis?: Redis
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What are the consequences of making this required?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

None, I made it required 👍

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to change the specs here? or was it leftover from a merge?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leftover from the rebase, fixed it in the latest commit

@sanducb sanducb requested review from mkurapov and njlie April 2, 2026 09:16
Copy link
Copy Markdown
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Very minor suggestions

partialIncomingPaymentId: uuid(),
expiresAt: prepare.expiresAt
}
)) as PartialPaymentDecision
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
)) as PartialPaymentDecision
))

Comment on lines +84 to +89
options?: {
dataToTransmit?: string
partialIncomingPaymentId?: string
expiresAt?: Date
}
): Promise<PartialPaymentDecision>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
options?: {
dataToTransmit?: string
partialIncomingPaymentId?: string
expiresAt?: Date
}
): Promise<PartialPaymentDecision>
options: {
dataToTransmit: string
partialIncomingPaymentId: string
expiresAt: Date
}
): Promise<PartialPaymentDecision>

@sanducb sanducb requested a review from mkurapov April 6, 2026 11:04
@sanducb sanducb merged commit e8c8822 into feature/encrypted-data-exchange Apr 6, 2026
32 of 52 checks passed
@sanducb sanducb deleted the bs/raf-1185 branch April 6, 2026 14:41
sanducb added a commit that referenced this pull request Apr 7, 2026
… frames (#3706)

* feat(backend): add middleware that handles STREAM KYC/additional data payloads - wip

* feat(backend): add wip comment

* feat: support arbitrary data on prepare packets - WIP

* feat(pay): add controller to handle additional data in STREAM packets

* feat: add STREAM connection id in cache key for KYC response, remove unused env vars

* chore: address PR comments

* chore: add tests for payment decision middleware and interledgerjs changes

* fix: update app data controller ti return F99

* feat: update additional data middleware logic, add pay and stream-receiver updates

* chore(backend): use updated pay and stream-receiver from npm, update lockfile

* feat(backend): refactor partial payment middleware, add handler for partial payments to mock ASE

* fix(backend): fix build errors in CI

* fix(backend): formatting

* fix(backend): formatting

* fix(backend): partial payment decision middleware tests

* fix(backend): formatting

* feat(backend): address review comments

* feat(backend): address PR comments

* fix(backend): fix failing CI tests

* chore: align open-payments-specifications with feature/encrypted-data-exchange

* fix: webhooks tests

* fix: delete flaky test

* fix: backend build

* chore: address review comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: backend Changes in the backend package. pkg: documentation Changes in the documentation package. pkg: mock-account-service-lib pkg: mock-ase type: source Changes business logic type: tests Testing related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants