Skip to content

Commit 1aca716

Browse files
committed
chore(ocp): document Transactor's; update verified state resolution to requery right before transfers
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent bf65a57 commit 1aca716

4 files changed

Lines changed: 113 additions & 9 deletions

File tree

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

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,30 @@ internal class GiveBillTransactor(
7272
data = payloadInfo.codeData.toList()
7373
}
7474

75+
/**
76+
* Presents a cash bill and waits for a recipient to claim it via peer-to-peer
77+
* messaging, then executes the on-chain transfer.
78+
*
79+
* Flow:
80+
* 1. Resolve a [VerifiedState] for the currency/token pair using a fallback
81+
* chain: provided state -> local proto store -> live mint data fetch.
82+
* 2. Compute exchange data from the verified state (fails if the rate has
83+
* expired past [exchangeDataTimeout]).
84+
* 3. Publish a "give bill" request on the rendezvous messaging stream,
85+
* advertising the token mint and exchange data to potential recipients.
86+
* 4. Block until a "grab bill" response arrives on the same rendezvous stream.
87+
* 5. Verify the grab request's destination signature against the rendezvous
88+
* key to ensure the destination hasn't been tampered with.
89+
* 6. Guard against duplicate transfers (same receiving account seen twice).
90+
* 7. Transfer funds from the sender's token vault to the recipient's
91+
* destination account.
92+
* 8. Poll for the intent metadata confirmation from the server.
93+
*
94+
* Preconditions: [with] must be called first to set the token, amount, owner,
95+
* and (optionally) a pre-resolved [VerifiedState].
96+
*
97+
* @return the confirmed [TransactionMetadata.SendPublicPayment] on success.
98+
*/
7599
suspend fun start(): Result<TransactionMetadata.SendPublicPayment> {
76100
val ownerKey = owner
77101
?: return logAndFail(GiveTransactorError.Other(message = "No owner key. Did you call with() first?"))
@@ -99,8 +123,10 @@ internal class GiveBillTransactor(
99123
billExchangeDataTimeout = exchangeDataTimeout
100124
) ?: return logAndFail(GiveTransactorError.ExchangeRateExpiredException())
101125

102-
// 1. Send request to "give" the bill to the recipient
103-
// This provides the recipient with the desired token mint of the cash
126+
// 1. Send request to "give" the bill to the recipient.
127+
// This provides the recipient with the desired token mint of the cash.
128+
// If this fails, bail out immediately — the receiver never got the
129+
// advertisement so the stream will never deliver a grab request.
104130
messagingController.sendRequestToGiveBill(desiredToken.address, rendezvous, exchangeData)
105131
.onSuccess {
106132
trace(
@@ -109,12 +135,9 @@ internal class GiveBillTransactor(
109135
type = TraceType.Log
110136
)
111137
}.onFailure { cause ->
112-
trace(
113-
tag = "Messaging",
114-
message = "Failed to send request to give bill for ${desiredToken.symbol}",
115-
type = TraceType.Error,
116-
error = cause
117-
)
138+
return logAndFail(cause) {
139+
"token" to desiredToken.symbol
140+
}
118141
}
119142

120143
trace(
@@ -150,6 +173,23 @@ internal class GiveBillTransactor(
150173

151174
receivingAccount = transferRequest.account
152175

176+
/**
177+
* At transfer time:
178+
* 1. If providedVerifiedState exists → use it (caller knows best)
179+
* 2. Otherwise → try fresh from proto store
180+
* 3. Otherwise → fall back to the upfront-resolved state
181+
*/
182+
val transferVerifiedState = providedVerifiedState
183+
?: verifiedProtoManager.getVerifiedStateFor(sendingAmount.rate.currency, desiredToken.address)
184+
?: verifiedState
185+
186+
val transferExchangeData = transferVerifiedState.exchangeDataFor(
187+
amount = sendingAmount,
188+
mint = desiredToken.address,
189+
billExchangeDataTimeout = exchangeDataTimeout
190+
) ?: exchangeData
191+
192+
153193
// 4. Send the funds to destination
154194
return transactionController.transfer(
155195
scope = scope,
@@ -158,7 +198,7 @@ internal class GiveBillTransactor(
158198
source = sendingVault,
159199
destination = transferRequest.account,
160200
rendezvous = rendezvous.toPublicKey(),
161-
exchangeData = exchangeData,
201+
exchangeData = transferExchangeData,
162202
).fold(
163203
onSuccess = {
164204
transactionController.pollIntentMetadata(

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,31 @@ internal class GrabBillTransactor(
3030
this.payload = payload
3131
}
3232

33+
/**
34+
* Claims a cash bill from a sender, handling both legacy single-mint and
35+
* multi-mint payment flows.
36+
*
37+
* Flow (dispatched by [PayloadKind]):
38+
*
39+
* **Legacy Cash** — sends a grab request directly, then polls for the
40+
* intent confirmation.
41+
*
42+
* **MultiMintCash:**
43+
* 1. Poll the rendezvous messaging stream for the sender's "give" request
44+
* to learn which token mint and exchange data to use.
45+
* 2. Resolve token metadata for the proposed mint.
46+
* 3. Create a user account for that token if one doesn't exist yet.
47+
* 4. Send a "grab bill" request back to the sender with this account's
48+
* vault as the destination.
49+
* 5. Poll for intent confirmation, copying in the verified exchange data
50+
* received from the give request.
51+
* 6. Acknowledge the give-request message to clear it from the stream.
52+
*
53+
* Preconditions: [with] must be called first to set the owner cluster and
54+
* the scanned [OpenCodePayload].
55+
*
56+
* @return the confirmed [TransactionMetadata.PublicPayment] on success.
57+
*/
3358
suspend fun start(): Result<TransactionMetadata.PublicPayment> {
3459
val ownerKey = owner
3560
?: return logAndFail(GrabTransactorError.Other(message = "No owner cluster available. Did you call with() first?"))

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,31 @@ internal class ReceiveGiftCardTransactor(
3939
)
4040
}
4141

42+
/**
43+
* Claims a gift card by its entropy — looks up the on-chain state, validates
44+
* eligibility, and transfers the balance into the receiver's vault.
45+
*
46+
* Flow:
47+
* 1. Derive the gift card's owner key pair from the entropy-based mnemonic.
48+
* 2. Query the server for the gift card's account info.
49+
* 3. Pre-claim checks:
50+
* - Fail if already claimed.
51+
* - Fail if expired or in an unknown state.
52+
* - If the current user is the issuer and [claimIfOwned] is false,
53+
* return [ReceiveGiftTransactorError.UsersGiftCard] so the caller
54+
* can prompt for confirmation.
55+
* 4. Resolve the token mint and metadata from the gift card's account info.
56+
* 5. Create a user account for that token if one doesn't exist yet.
57+
* 6. Submit a receive-remotely intent to transfer the gift card balance
58+
* into the receiver's token vault.
59+
*
60+
* Preconditions: [with] must be called first to set the owner cluster and
61+
* the gift card's base-58 entropy string.
62+
*
63+
* @param claimIfOwned when `true`, allows claiming even if the current user
64+
* issued the gift card (self-claim).
65+
* @return the claimed [Token] and its [LocalFiat] value on success.
66+
*/
4267
suspend fun start(claimIfOwned: Boolean): Result<Pair<Token, LocalFiat>> {
4368
val requestingOwner = owner
4469
?: return logAndFail(ReceiveGiftTransactorError.Other(message = "No owner key. Did you call with() first?"))

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ internal class SendGiftCardTransactor(
4444
data = payloadInfo.codeData.toList()
4545
}
4646

47+
/**
48+
* Funds a gift card for remote sending — submits a remote-send intent so the
49+
* gift card can later be claimed by a recipient via link or QR code.
50+
*
51+
* Flow:
52+
* 1. Resolve the sender's token vault (timelock account for the target token).
53+
* 2. Submit a remote-send intent to the server, transferring funds from
54+
* the sender's vault into the pre-created [GiftCardAccount].
55+
*
56+
* Preconditions: [with] must be called first to set the gift card account,
57+
* amount, token, and owner cluster.
58+
*
59+
* @return the [IntentRemoteSend] details on success.
60+
*/
4761
suspend fun start(): Result<IntentRemoteSend> {
4862
val rendezvous = rendezvousKey
4963
?: return logAndFail(GiveTransactorError.Other(message = "No rendezvous key. Did you call with() first?"))

0 commit comments

Comments
 (0)