Skip to content

Commit 9195d70

Browse files
committed
refactor(oc): introduce BillPresentationData to bundle code data + nonce
Replace separate `data` and `usedNonce` properties on GiveBillTransactor with a single BillPresentationData model, threading it through BillTransactionManager, BillController, and RealSessionController. This makes the pairing explicit and prevents accidental misuse when a bill is re-presented with the same nonce after a cancelled share-sheet. Also adds KDoc across the full give/grab/send/receive flow where it was missing — transactors, BillTransactionManager, BillController, and supporting types. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent db7b330 commit 9195d70

13 files changed

Lines changed: 257 additions & 14 deletions

File tree

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/bill/BillState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ sealed interface Bill {
124124
override val data: List<Byte> = emptyList(),
125125
val kind: Kind = Kind.cash,
126126
val verifiedState: VerifiedState? = null,
127+
val nonce: List<Byte> = emptyList(),
127128
) : Bill {
128129
override val canFlip: Boolean = false
129130
}

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/internal/bill/BillController.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.flipcash.app.userflags.UserFlagsCoordinator
55
import com.flipcash.services.user.UserManager
66
import com.getcode.opencode.internal.manager.VerifiedState
77
import com.getcode.opencode.model.accounts.AccountCluster
8+
import com.getcode.opencode.internal.transactors.BillPresentationData
89
import com.getcode.opencode.managers.BillTransactionManager
910
import com.getcode.opencode.model.accounts.GiftCardAccount
1011
import com.getcode.opencode.model.core.OpenCodePayload
@@ -17,6 +18,11 @@ import kotlinx.coroutines.flow.update
1718
import javax.inject.Inject
1819
import javax.inject.Singleton
1920

21+
/**
22+
* UI-facing facade for bill transactions. Owns the [BillState] flow and
23+
* delegates to [BillTransactionManager] for all four payment flows (give,
24+
* grab, send cash link, receive cash link).
25+
*/
2026
@Singleton
2127
class BillController @Inject constructor(
2228
private val transactionManager: BillTransactionManager,
@@ -37,12 +43,18 @@ class BillController @Inject constructor(
3743
transactionManager.reset()
3844
}
3945

46+
/**
47+
* Initiates the **give** flow — presents a bill and waits for a recipient to
48+
* grab it. Delegates to [BillTransactionManager.awaitGrabFromRecipient],
49+
* injecting the configured exchange-data timeout from user flags.
50+
*/
4051
fun awaitGrab(
4152
amount: LocalFiat,
4253
token: Token,
4354
owner: AccountCluster,
4455
verifiedState: VerifiedState?,
45-
present: (List<Byte>) -> Unit,
56+
nonce: List<Byte>? = null,
57+
present: (BillPresentationData) -> Unit,
4658
onGrabbed: suspend (LocalFiat) -> Unit,
4759
onTimeout: () -> Unit,
4860
onError: (Throwable) -> Unit,
@@ -52,6 +64,7 @@ class BillController @Inject constructor(
5264
owner = owner,
5365
verifiedState = verifiedState,
5466
billExchangeDataTimeout = userFlags.resolvedFlags.value.billExchangeDataTimeout.effectiveValue,
67+
nonce = nonce,
5568
present = present,
5669
onGrabbed = onGrabbed,
5770
onTimeout = onTimeout,
@@ -60,13 +73,15 @@ class BillController @Inject constructor(
6073

6174
fun cancelAwaitForGrab() = transactionManager.cancelAwaitForGrab()
6275

76+
/** Initiates the **grab** flow — claims a scanned bill from a sender. */
6377
fun attemptGrab(
6478
owner: AccountCluster,
6579
payload: OpenCodePayload,
6680
onGrabbed: suspend (Token, LocalFiat, VerifiedState?) -> Unit,
6781
onError: (Throwable) -> Unit,
6882
) = transactionManager.attemptGrabFromSender(owner, payload, onGrabbed, onError)
6983

84+
/** Initiates the **send cash link** flow — funds a gift card for remote sending. */
7085
fun fundGiftCard(
7186
giftCard: GiftCardAccount,
7287
amount: LocalFiat,
@@ -76,6 +91,7 @@ class BillController @Inject constructor(
7691
onError: (Throwable) -> Unit,
7792
) = transactionManager.fundGiftCard(giftCard, amount, owner, token, onFunded, onError)
7893

94+
/** Initiates the **receive cash link** flow — claims a gift card by entropy. */
7995
fun receiveGiftCard(
8096
entropy: String,
8197
owner: AccountCluster,

apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ class RealSessionController @Inject constructor(
358358
amount = bill.amount,
359359
token = bill.token,
360360
verifiedState = (bill as? Bill.Cash)?.verifiedState,
361+
nonce = (bill as? Bill.Cash)?.nonce?.takeIf { it.isNotEmpty() },
361362
owner = owner,
362363
onGrabbed = { amount ->
363364
tokenCoordinator.subtract(bill.token, amount)
@@ -383,7 +384,7 @@ class RealSessionController @Inject constructor(
383384
message = resources.getString(R.string.error_description_CashReturnedToWallet)
384385
)
385386
},
386-
present = { data ->
387+
present = { (data, nonce) ->
387388
if (!bill.didReceive) {
388389
trace(
389390
tag = "Session",
@@ -398,7 +399,7 @@ class RealSessionController @Inject constructor(
398399
type = TraceType.User,
399400
)
400401
}
401-
presentBillToUser(data, bill)
402+
presentBillToUser(data, nonce, bill)
402403
},
403404
)
404405
}
@@ -818,7 +819,7 @@ class RealSessionController @Inject constructor(
818819
)
819820
}
820821

821-
private fun presentBillToUser(data: List<Byte>, bill: Bill) {
822+
private fun presentBillToUser(data: List<Byte>, nonce: List<Byte>, bill: Bill) {
822823
if (billController.state.value.bill != null) return
823824

824825
billController.update {
@@ -830,6 +831,7 @@ class RealSessionController @Inject constructor(
830831
confirmationDelay = bill.confirmationDelay,
831832
token = bill.token,
832833
verifiedState = (bill as? Bill.Cash)?.verifiedState,
834+
nonce = nonce,
833835
),
834836
valuation = PaymentValuation(bill.amount.nativeAmount),
835837
)

apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/SessionControllerGiftCardErrorTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ class SessionControllerGiftCardErrorTest {
170170
token = any(),
171171
owner = any(),
172172
verifiedState = any(),
173+
nonce = any(),
173174
present = any(),
174175
onGrabbed = any(),
175176
onTimeout = any(),
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.getcode.opencode.internal.transactors
2+
3+
/**
4+
* Bundles the scannable code data with the nonce used to derive it.
5+
*
6+
* Keeping these paired ensures the same nonce is reused when a bill is
7+
* re-presented (e.g. after a cancelled share-sheet), preserving the
8+
* original rendezvous key so the sender's messaging stream stays valid.
9+
*/
10+
data class BillPresentationData(
11+
val data: List<Byte>,
12+
val nonce: List<Byte>,
13+
)

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ import kotlinx.coroutines.CoroutineScope
2525
import kotlinx.coroutines.cancel
2626
import kotlin.time.Duration
2727

28+
/**
29+
* Transactor for the **give** side of a peer-to-peer cash bill.
30+
*
31+
* Lifecycle: call [with] to configure the bill parameters and generate
32+
* a rendezvous payload, then [start] to advertise the bill on the
33+
* messaging stream and block until a recipient grabs it and the on-chain
34+
* transfer completes. Call [dispose] to tear down the coroutine scope
35+
* and clear state when the bill is dismissed or times out.
36+
*/
2837
internal class GiveBillTransactor(
2938
private val currencyController: CurrencyController,
3039
private val messagingController: MessagingController,
@@ -43,15 +52,23 @@ internal class GiveBillTransactor(
4352

4453
private var providedVerifiedState: VerifiedState? = null
4554

46-
var data: List<Byte> = emptyList()
55+
var presentationData: BillPresentationData = BillPresentationData(emptyList(), emptyList())
4756
private set
4857

58+
/**
59+
* Configures this transactor for a new bill and generates the rendezvous
60+
* payload. Must be called before [start].
61+
*
62+
* @param providedNonce optional nonce to reuse from a previous presentation
63+
* of the same bill. When `null` a fresh random nonce is generated.
64+
*/
4965
fun with(
5066
token: Token,
5167
amount: LocalFiat,
5268
owner: AccountCluster,
5369
billExchangeDataTimeout: Duration?,
54-
verifiedState: VerifiedState?
70+
verifiedState: VerifiedState?,
71+
providedNonce: List<Byte>? = null,
5572
) {
5673
this.token = token
5774
this.amount = amount
@@ -61,14 +78,16 @@ internal class GiveBillTransactor(
6178

6279
receivingAccount = null
6380

81+
val resolvedNonce = providedNonce ?: nonce
82+
6483
val payloadResult = payloadFactory.create(
6584
kind = PayloadKind.MultiMintCash,
6685
value = amount.nativeAmount,
67-
nonce = nonce
86+
nonce = resolvedNonce
6887
)
6988

7089
rendezvousKey = payloadResult.rendezvous
71-
data = payloadResult.codeData
90+
presentationData = BillPresentationData(data = payloadResult.codeData, nonce = resolvedNonce)
7291
}
7392

7493
/**
@@ -196,13 +215,14 @@ internal class GiveBillTransactor(
196215
)
197216
}
198217

218+
/** Cancels the coroutine scope and clears all held state. */
199219
fun dispose() {
200220
owner = null
201-
data = emptyList()
221+
presentationData = BillPresentationData(emptyList(), emptyList())
202222
rendezvousKey = null
203223
receivingAccount = null
204224
token = null
205-
225+
providedVerifiedState = null
206226
scope.cancel()
207227
}
208228

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GrabBillTransactor.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ import com.getcode.utils.NotifiableError
1515
import kotlinx.coroutines.CoroutineScope
1616
import kotlinx.coroutines.cancel
1717

18+
/**
19+
* Transactor for the **grab** side of a peer-to-peer cash bill.
20+
*
21+
* Lifecycle: call [with] to set the owner and scanned payload, then
22+
* [start] to claim the bill from the sender. For multi-mint payloads
23+
* this involves polling the rendezvous stream, creating a token account
24+
* if needed, and sending the grab request back. Call [dispose] to tear
25+
* down state when finished.
26+
*/
1827
internal class GrabBillTransactor(
1928
private val accountController: AccountController,
2029
private val messagingController: MessagingController,
@@ -25,6 +34,7 @@ internal class GrabBillTransactor(
2534
private var owner: AccountCluster? = null
2635
private var payload: OpenCodePayload? = null
2736

37+
/** Configures this transactor with the owner and scanned payload. Must be called before [start]. */
2838
fun with(owner: AccountCluster, payload: OpenCodePayload) {
2939
this.owner = owner
3040
this.payload = payload
@@ -72,6 +82,7 @@ internal class GrabBillTransactor(
7282
}
7383
}
7484

85+
/** Cancels the coroutine scope and clears all held state. */
7586
fun dispose() {
7687
owner = null
7788
payload = null

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/PayloadFactory.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@ import com.getcode.opencode.model.core.PayloadKind
66
import com.getcode.opencode.model.financial.Fiat
77
import javax.inject.Inject
88

9+
/**
10+
* The output of [PayloadFactory.create] — a rendezvous key pair derived from
11+
* the payload and the encoded scannable code data.
12+
*/
913
data class PayloadResult(
1014
val rendezvous: KeyPair,
1115
val codeData: List<Byte>,
1216
)
1317

18+
/**
19+
* Creates an [OpenCodePayload][com.getcode.opencode.model.core.OpenCodePayload]
20+
* from the given parameters and returns the derived rendezvous key pair and
21+
* encoded code data. Extracted as a functional interface for testability.
22+
*/
1423
fun interface PayloadFactory {
1524
fun create(kind: PayloadKind, value: Fiat, nonce: List<Byte>): PayloadResult
1625
}

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ import com.getcode.utils.CodeServerError
1818
import com.getcode.utils.NotifiableError
1919
import com.getcode.utils.timedTraceSuspend
2020

21+
/**
22+
* Transactor for **receiving (claiming) a cash link** (gift card).
23+
*
24+
* Lifecycle: call [with] to set the owner and gift card entropy, then
25+
* [start] to look up the gift card on-chain, validate eligibility, and
26+
* transfer the balance into the receiver's vault. Call [dispose] to
27+
* clear state when finished.
28+
*/
2129
internal class ReceiveGiftCardTransactor(
2230
private val accountController: AccountController,
2331
private val transactionController: TransactionController,
@@ -32,6 +40,7 @@ internal class ReceiveGiftCardTransactor(
3240

3341
private var giftCardOwner: AccountCluster? = null
3442

43+
/** Configures this transactor with the owner and gift card entropy. Must be called before [start]. */
3544
fun with(owner: AccountCluster, entropy: String) {
3645
this.owner = owner
3746
mnemonic = mnemonicManager.fromEntropyBase58(entropy)
@@ -145,6 +154,7 @@ internal class ReceiveGiftCardTransactor(
145154
}
146155
}
147156

157+
/** Clears all held state. */
148158
fun dispose() {
149159
owner = null
150160
giftCardAccount = null

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/SendGiftCardTransactor.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import com.getcode.opencode.utils.nonce
1414
import com.getcode.utils.CodeServerError
1515
import com.getcode.utils.NotifiableError
1616

17+
/**
18+
* Transactor for **sending a cash link** (gift card).
19+
*
20+
* Lifecycle: call [with] to set the gift card account, amount, token,
21+
* and owner, then [start] to submit the remote-send intent that funds
22+
* the gift card on-chain. Call [dispose] to clear state when finished.
23+
*/
1724
internal class SendGiftCardTransactor(
1825
private val transactionController: TransactionController,
1926
private val payloadFactory: PayloadFactory,
@@ -25,6 +32,7 @@ internal class SendGiftCardTransactor(
2532

2633
private var rendezvousKey: KeyPair? = null
2734

35+
/** Configures this transactor for a new gift card send. Must be called before [start]. */
2836
fun with(giftCard: GiftCardAccount, amount: LocalFiat, token: Token, owner: AccountCluster) {
2937
this.giftCardAccount = giftCard
3038
this.token = token
@@ -84,6 +92,7 @@ internal class SendGiftCardTransactor(
8492

8593
}
8694

95+
/** Clears all held state. */
8796
fun dispose() {
8897
amount = null
8998
giftCardAccount = null

0 commit comments

Comments
 (0)