Skip to content

Commit a071a33

Browse files
committed
OC evidenced promise
1 parent d734274 commit a071a33

6 files changed

Lines changed: 159 additions & 58 deletions

File tree

obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3487,7 +3487,7 @@ object Http4s700 {
34873487
"Attach Open Corridor Promise Evidence",
34883488
"""Attach on-chain promise evidence to a PENDING OPEN_CORRIDOR_PROMISE Transaction Request.
34893489
|
3490-
|Called by the bank's own Bank Node (machine-to-machine) after it has written the Promise commitment to the blockchain. The body carries the transaction hash of the on-chain write plus the commit–reveal evidence: the `commitment` (the hash written on-chain), the `salt`, and the `preimage`. OBP-API stores these as Transaction Request attributes and later relays them to the beneficiary bank inside the `obp_credit_notification` message, enabling the beneficiary to verify `SHA-256(salt ‖ preimage)` against the on-chain commitment without the originating bank's cooperation.
3490+
|Called by the bank's own Bank Node (machine-to-machine) after it has written the Promise commitment to the blockchain. The body carries the transaction hash of the on-chain write plus the commit–reveal evidence: the `commitment` (the hash written on-chain), the `salt`, and the `preimage`. OBP-API stores these as Transaction Request attributes and immediately relays them to the beneficiary bank inside the `obp_credit_notification` message (enqueued to the transactional outbox on the first successful attach), enabling the beneficiary to verify `SHA-256(salt ‖ preimage)` against the on-chain commitment without the originating bank's cooperation — and to credit its customer ahead of settlement on the strength of that verified promise.
34913491
|
34923492
|The evidence fields are opaque strings to OBP-API — they are stored and relayed verbatim, never parsed.
34933493
|
@@ -3608,6 +3608,7 @@ object Http4s700 {
36083608
)
36093609
val createAccountResponseExampleV700 = JSONFactory700.CreateAccountResponseJsonV700(
36103610
account_id = "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0",
3611+
bank_id = "gh.29.uk",
36113612
user_id = "9ca9a7e4-6d02-40e3-a129-0b2bf89de9b1",
36123613
label = "My Account",
36133614
product_code = "OPEN_CORRIDOR",
@@ -3836,11 +3837,13 @@ object Http4s700 {
38363837
|
38373838
|Computes `net = SUM(PENDING A→B promises) − SUM(PENDING B→A promises)`, mints one internal OPEN_CORRIDOR_SETTLEMENT Transaction Request between the pair's settlement accounts whose execution posts ONE net Transaction (debtor's outgoing settlement account → creditor's incoming), records that Transaction's id on each covered promise in the `settled_by_transaction_ids` attribute (and the settlement TR's id in `settled_by_transaction_request_id`), and sets the covered promises to COMPLETED. N promises collapse into one settlement — that compression is the netting.
38383839
|
3839-
|In the same database transaction, the Interface C messages are written to the transactional outbox: one `obp_credit_notification` per covered promise to its beneficiary bank (relaying the commit–reveal evidence triplet), and one `obp_settlement_instruction` for the net amount to the debtor bank. The outbox relay publishes them and records each bank's reply.
3840+
|Only promises whose on-chain evidence has been attached are covered: an unevidenced promise generated no credit notification and no beneficiary payout, so netting it would move value for a payment nobody delivered — it stays PENDING for a later cycle.
3841+
|
3842+
|In the same database transaction, the Interface C messages are written to the transactional outbox: one `obp_settlement_advice` per beneficiary bank listing the covered promise ids it already paid out against (credit notifications travel at promise-report-back time, not here), and one `obp_settlement_instruction` for the net amount to the debtor bank. The outbox relay publishes them and records each bank's reply.
38403843
|
38413844
|NOTE: the posted net Transaction deliberately does not mirror any single covered promise — it can differ in direction, amount and accounts. Reconciliation must follow the `settled_by_transaction_ids` linkage, never assume the Transaction matches the promise body.
38423845
|
3843-
|A trigger for a pair with no PENDING promises is a no-op. When the flows offset exactly (net zero) the promises are discharged with no Transaction posted and no settlement instruction sent — the credit notifications still go out.
3846+
|A trigger for a pair with no PENDING evidenced promises is a no-op. When the flows offset exactly (net zero) the promises are discharged with no Transaction posted and no settlement instruction sent — the settlement advices still go out.
38443847
|
38453848
|`net_amount` is always the absolute value; direction is carried by `debtor_bank_id` → `creditor_bank_id` (assigned from the sign of the net). Either bank in the pair may trigger settlement — the role is checked at the URL's BANK_ID, and who ends up debtor is decided by the net, not by who called.
38463849
|
@@ -3857,7 +3860,7 @@ object Http4s700 {
38573860
currency = "KES",
38583861
net_amount = "2500.00",
38593862
covered_transaction_request_ids = List("4050046c-63b3-4868-8a22-14b4181d33a6"),
3860-
credit_notifications_enqueued = 3,
3863+
settlement_advices_enqueued = 1,
38613864
settlement_instructions_enqueued = 1
38623865
),
38633866
List($AuthenticatedUserIsRequired, UserHasMissingRoles, OpenCorridorDisabled, InvalidJsonFormat, InvalidJsonValue,
@@ -3896,7 +3899,7 @@ object Http4s700 {
38963899
|* `ledger_status` — the OBP-side OPEN_CORRIDOR_SETTLEMENT Transaction Request (COMPLETED at settle time: netting, promise discharge and the net ledger Transaction are done).
38973900
|* `settlement_status` — the value leg on the rail, as last reported by the debtor bank's node: `NET_ZERO` (nothing to move), `INSTRUCTED` (no node reply yet), `SETTLING` / `SUBMITTED` (in flight, with `settlement_depth` = confirmation depth when reported), `FINAL` (node reported finality), `ERROR` (non-retryable node error; operator reconciliation — see the message's `last_error`).
38983901
|
3899-
|`messages` lists the settlement's Interface C outbox rows (credit notifications and the settlement instruction) with their delivery state.
3902+
|`messages` lists the settlement's Interface C outbox rows (settlement advices and the settlement instruction) with their delivery state.
39003903
|
39013904
|Requires `open_corridor_enabled=true` on this instance and the `CanSettleOpenCorridor` role at BANK_ID.
39023905
|

obp-api/src/main/scala/code/api/v7_0_0/JSONFactory7.0.0.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,7 @@ object JSONFactory700 extends MdcLoggable with code.api.util.CustomJsonFormats {
11321132

11331133
case class CreateAccountResponseJsonV700(
11341134
account_id: String,
1135+
bank_id: String,
11351136
user_id: String,
11361137
label: String,
11371138
product_code: String,
@@ -1148,6 +1149,7 @@ object JSONFactory700 extends MdcLoggable with code.api.util.CustomJsonFormats {
11481149
): CreateAccountResponseJsonV700 =
11491150
CreateAccountResponseJsonV700(
11501151
account_id = account.accountId.value,
1152+
bank_id = account.bankId.value,
11511153
user_id = userId,
11521154
label = account.label,
11531155
product_code = account.accountType,
@@ -1212,7 +1214,7 @@ object JSONFactory700 extends MdcLoggable with code.api.util.CustomJsonFormats {
12121214
currency: String,
12131215
net_amount: String,
12141216
covered_transaction_request_ids: List[String],
1215-
credit_notifications_enqueued: Int,
1217+
settlement_advices_enqueued: Int,
12161218
settlement_instructions_enqueued: Int
12171219
)
12181220

obp-api/src/main/scala/code/bankconnectors/opencorridor/OpenCorridorProcessor.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import code.api.util.{APIUtil, CallContext, NewStyle}
88
import code.api.v7_0_0.JSONFactory700.{OpenCorridorPromiseJsonV700, PostOpenCorridorPromiseJsonV700, TransactionRequestBodyOpenCorridorJsonV700}
99
import code.util.Helper
1010
import com.openbankproject.commons.ExecutionContext.Implicits.global
11+
import com.openbankproject.commons.dto.{OpenCorridorMoneyValue, OpenCorridorOriginator, OutBoundOpenCorridorCreditNotification}
1112
import com.openbankproject.commons.model._
1213
import com.openbankproject.commons.model.enums.ChallengeType.OBP_TRANSACTION_REQUEST_CHALLENGE
1314
import com.openbankproject.commons.model.enums.{TransactionRequestAttributeType, TransactionRequestStatus, TransactionRequestTypes}
15+
import code.transactionrequests.MappedTransactionRequest
16+
import net.liftweb.mapper.By
1417

1518
import java.util.Date
1619
import org.json4s.native.Serialization.write
@@ -187,6 +190,11 @@ object OpenCorridorProcessor {
187190
NewStyle.function.createTransactionRequestAttributes(
188191
bankId, transactionRequestId, attributes, isPersonal = false, callContext
189192
) map { case (_, callContext) =>
193+
// First attach only (idempotent redeliveries skip this branch): the
194+
// promise now exists on-chain, so the beneficiary bank gets its
195+
// evidence-bearing credit notification immediately — the promise is
196+
// what gives it the confidence to pay out ahead of settlement.
197+
enqueueCreditNotification(transactionRequestId, submittedEvidence)
190198
(buildPromiseJson(tr, submittedEvidence, user.userId, reportedAt), callContext)
191199
}
192200
} else {
@@ -198,6 +206,36 @@ object OpenCorridorProcessor {
198206
} yield (promiseJson, callContext)
199207
}
200208

209+
private implicit val wireFormats: Formats = Serialization.formats(NoTypeHints)
210+
211+
/** Build and enqueue the `obp_credit_notification` for a promise whose evidence
212+
* was just attached. The outbox row's correlation id is the promise TR id
213+
* (settlement-scoped messages use the settlement id there instead). */
214+
private def enqueueCreditNotification(
215+
transactionRequestId: TransactionRequestId,
216+
evidence: Map[String, String]
217+
): Unit =
218+
MappedTransactionRequest
219+
.find(By(MappedTransactionRequest.mTransactionRequestId, transactionRequestId.value))
220+
.foreach { row =>
221+
val wireBody = OutBoundOpenCorridorCreditNotification(
222+
transaction_request_id = transactionRequestId.value,
223+
value = OpenCorridorMoneyValue(row.mBody_Value_Currency.get, row.mBody_Value_Amount.get),
224+
description = Option(row.mBody_Description.get).filter(_.nonEmpty),
225+
originator = Option(row.mOriginator_Name.get).filter(_.nonEmpty).map(name =>
226+
OpenCorridorOriginator(name, Option(row.mOriginator_Address.get).filter(_.nonEmpty))),
227+
netting_snapshot_id = None,
228+
promise_id = evidence.get(PromiseAttributeTxHash),
229+
promise_blockchain = evidence.get(PromiseAttributeBlockchain),
230+
promise_commitment = evidence.get(PromiseAttributeCommitment),
231+
promise_salt = evidence.get(PromiseAttributeSalt),
232+
promise_preimage = evidence.get(PromiseAttributePreimage)
233+
)
234+
OpenCorridorOutbox.enqueue(
235+
transactionRequestId.value, "obp_credit_notification", row.mTo_BankId.get,
236+
Serialization.write(wireBody))
237+
}
238+
201239
private def buildPromiseJson(
202240
tr: TransactionRequest,
203241
evidence: Map[String, String],

obp-api/src/main/scala/code/bankconnectors/opencorridor/OpenCorridorSettlement.scala

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ object OpenCorridorSettlement extends MdcLoggable {
8585
if (DoobieTransactionRequestQueries.lockTransactionRequest(trId).isEmpty) {
8686
logger.warn(s"Open Corridor settle: could not lock promise TR $trId — skipping")
8787
None
88+
} else if (!hasPromiseEvidence(trId)) {
89+
// No on-chain evidence yet means the beneficiary bank was never
90+
// notified and never paid out — netting it would move value between
91+
// banks for a payment nobody delivered. It stays PENDING for a
92+
// later cycle, once the originating node reports back.
93+
logger.info(s"Open Corridor settle: promise TR $trId has no on-chain evidence yet — skipping")
94+
None
8895
} else {
8996
MappedTransactionRequest.find(By(MappedTransactionRequest.mTransactionRequestId, trId))
9097
.filter(_.mStatus.get == TransactionRequestStatus.PENDING.toString)
@@ -103,7 +110,7 @@ object OpenCorridorSettlement extends MdcLoggable {
103110
currency = currency,
104111
net_amount = "0",
105112
covered_transaction_request_ids = Nil,
106-
credit_notifications_enqueued = 0,
113+
settlement_advices_enqueued = 0,
107114
settlement_instructions_enqueued = 0
108115
), callContext))
109116
} else {
@@ -222,12 +229,23 @@ object OpenCorridorSettlement extends MdcLoggable {
222229
})
223230

224231
// Enqueue the Interface C messages in this same DB transaction (the outbox).
225-
creditNotifications <- Future.sequence(covered.map(row => buildCreditNotification(row, callContext)))
226-
_ <- Future {
227-
creditNotifications.foreach { case (beneficiaryBankId, wireBody) =>
232+
// Credit notifications went to each beneficiary at promise-report-back time
233+
// (OpenCorridorProcessor); settlement sends each beneficiary an advice so
234+
// its already-paid-out credits get marked settled.
235+
settlementAdviceCount <- Future {
236+
covered.groupBy(_.mTo_BankId.get).map { case (beneficiaryBankId, rows) =>
237+
val advice = OutBoundOpenCorridorSettlementAdvice(
238+
settlement_id = settlementTrId,
239+
currency = currency,
240+
net_amount = netAbs.toString(),
241+
debtor_bank_id = debtorBankId,
242+
creditor_bank_id = creditorBankId,
243+
covered_transaction_request_ids = rows.map(_.mTransactionRequestId.get),
244+
idempotency_key = settlementTrId
245+
)
228246
OpenCorridorOutbox.enqueue(
229-
settlementTrId, "obp_credit_notification", beneficiaryBankId, Serialization.write(wireBody))
230-
}
247+
settlementTrId, "obp_settlement_advice", beneficiaryBankId, Serialization.write(advice))
248+
}.size
231249
}
232250
settlementInstructionCount <- Future {
233251
if (netAbs > 0) {
@@ -259,12 +277,20 @@ object OpenCorridorSettlement extends MdcLoggable {
259277
currency = currency,
260278
net_amount = netAbs.toString(),
261279
covered_transaction_request_ids = covered.map(_.mTransactionRequestId.get),
262-
credit_notifications_enqueued = creditNotifications.size,
280+
settlement_advices_enqueued = settlementAdviceCount,
263281
settlement_instructions_enqueued = settlementInstructionCount
264282
), callContext)
265283
}
266284
}
267285

286+
/** True once the promise's on-chain evidence was attached (report-back done) —
287+
* the precondition for the beneficiary having been notified and paid out. */
288+
private def hasPromiseEvidence(trId: String): Boolean =
289+
TransactionRequestAttribute.find(
290+
By(TransactionRequestAttribute.Name, OpenCorridorProcessor.PromiseAttributeCommitment),
291+
By(TransactionRequestAttribute.TransactionRequestId, trId)
292+
).isDefined
293+
268294
/**
269295
* The GET view of one settlement (the resource minted by settlePair).
270296
*
@@ -347,31 +373,4 @@ object OpenCorridorSettlement extends MdcLoggable {
347373

348374
/** Build the credit notification for one covered promise, addressed to its
349375
* beneficiary (to-side) bank, relaying the §5.1 evidence attributes verbatim. */
350-
private def buildCreditNotification(
351-
row: MappedTransactionRequest,
352-
callContext: Option[CallContext]
353-
): Future[(String, OutBoundOpenCorridorCreditNotification)] = {
354-
val promiseTrId = TransactionRequestId(row.mTransactionRequestId.get)
355-
for {
356-
(attributes, _) <- NewStyle.function.getTransactionRequestAttributes(
357-
BankId(row.mFrom_BankId.get), promiseTrId, callContext)
358-
} yield {
359-
def attr(name: String): Option[String] =
360-
attributes.find(_.name == name).map(_.value).filter(_.nonEmpty)
361-
val wireBody = OutBoundOpenCorridorCreditNotification(
362-
transaction_request_id = promiseTrId.value,
363-
value = OpenCorridorMoneyValue(row.mBody_Value_Currency.get, row.mBody_Value_Amount.get),
364-
description = Option(row.mBody_Description.get).filter(_.nonEmpty),
365-
originator = Option(row.mOriginator_Name.get).filter(_.nonEmpty).map(name =>
366-
OpenCorridorOriginator(name, Option(row.mOriginator_Address.get).filter(_.nonEmpty))),
367-
netting_snapshot_id = None,
368-
promise_id = attr(OpenCorridorProcessor.PromiseAttributeTxHash),
369-
promise_blockchain = attr(OpenCorridorProcessor.PromiseAttributeBlockchain),
370-
promise_commitment = attr(OpenCorridorProcessor.PromiseAttributeCommitment),
371-
promise_salt = attr(OpenCorridorProcessor.PromiseAttributeSalt),
372-
promise_preimage = attr(OpenCorridorProcessor.PromiseAttributePreimage)
373-
)
374-
(row.mTo_BankId.get, wireBody)
375-
}
376-
}
377376
}

0 commit comments

Comments
 (0)