Skip to content

Commit 6527fe7

Browse files
committed
feat(opencode): record give/grab stage spans on the shared PaymentTrace
Wrap the grab (poll give-request, token metadata, account creation, request-grab) and give (resolve exchange data, send request, await grab, submit intent, poll metadata) stages in spans on the shared PaymentTrace, keyed by the rendezvous public key. Restores the PR #660 account-recovery rationale comment folded in during the grab-span refactor. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent f4c2b7c commit 6527fe7

2 files changed

Lines changed: 77 additions & 68 deletions

File tree

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

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import com.getcode.solana.keys.PublicKey
1818
import com.getcode.utils.CodeServerError
1919
import com.getcode.utils.NotifiableError
2020
import com.getcode.utils.TraceType
21+
import com.getcode.utils.payment.PaymentFlow
22+
import com.getcode.utils.payment.PaymentTraceRegistry
23+
import com.getcode.utils.payment.spanSuspendOrRun
2124
import com.getcode.utils.trace
2225
import kotlinx.coroutines.CoroutineScope
2326
import kotlinx.coroutines.cancel
@@ -123,12 +126,15 @@ internal class GiveBillTransactor(
123126
?: return logAndFail(GiveTransactorError.Other(message = "No token. Did you call with() first?"))
124127
val rendezvous = rendezvousKey
125128
?: return logAndFail(GiveTransactorError.Other(message = "No rendezvous key. Did you call with() first?"))
129+
val correlationId: String = rendezvous.publicKey
130+
val paymentTrace = PaymentTraceRegistry.open(correlationId, PaymentFlow.Give)
126131
val sendingAmount = amount
127132
?: return logAndFail(GiveTransactorError.Other(message = "No amount. Did you call with() first?"))
128133

129-
val initialState = providedVerifiedState
130-
?: verifiedFiatCalculator.resolveVerifiedState(sendingAmount.rate.currency, desiredToken.address)
131-
?: return logAndFail(GiveTransactorError.Other("Failed to get verified state"))
134+
val initialState = paymentTrace.spanSuspendOrRun("resolveExchangeData") {
135+
providedVerifiedState
136+
?: verifiedFiatCalculator.resolveVerifiedState(sendingAmount.rate.currency, desiredToken.address)
137+
} ?: return logAndFail(GiveTransactorError.Other("Failed to get verified state"))
132138

133139
val (verifiedState, exchangeData) = initialState.exchangeDataFor(
134140
amount = sendingAmount,
@@ -154,7 +160,9 @@ internal class GiveBillTransactor(
154160
// This provides the recipient with the desired token mint of the cash.
155161
// If this fails, bail out immediately — the receiver never got the
156162
// advertisement so the stream will never deliver a grab request.
157-
messagingController.sendRequestToGiveBill(desiredToken.address, rendezvous, exchangeData)
163+
paymentTrace.spanSuspendOrRun("sendRequestToGiveBill") {
164+
messagingController.sendRequestToGiveBill(desiredToken.address, rendezvous, exchangeData)
165+
}
158166
.onSuccess {
159167
trace(
160168
tag = "Messaging",
@@ -174,8 +182,12 @@ internal class GiveBillTransactor(
174182
)
175183

176184
// 2. Wait for recipient to grab the bill
177-
val transferRequest = messagingController.awaitRequestToGrabBill(scope, rendezvous)
178-
?: return logAndFail(GiveTransactorError.NoGrabReceived())
185+
val transferRequest = paymentTrace.spanSuspendOrRun("awaitGrab") {
186+
messagingController.awaitRequestToGrabBill(scope, rendezvous)
187+
} ?: run {
188+
PaymentTraceRegistry.finish(correlationId, success = false)
189+
return logAndFail(GiveTransactorError.NoGrabReceived())
190+
}
179191

180192

181193
// 3. Validate that destination hasn't been tampered with by
@@ -208,25 +220,31 @@ internal class GiveBillTransactor(
208220

209221

210222
// 4. Send the funds to destination
211-
return transactionController.transfer(
212-
scope = scope,
213-
amount = amount!!,
214-
mint = desiredToken.address,
215-
source = sendingVault,
216-
destination = transferRequest.account,
217-
rendezvous = rendezvous.toPublicKey(),
218-
exchangeData = transferExchangeData,
219-
).fold(
223+
val outcome = paymentTrace.spanSuspendOrRun("submitIntent") {
224+
transactionController.transfer(
225+
scope = scope,
226+
amount = amount!!,
227+
mint = desiredToken.address,
228+
source = sendingVault,
229+
destination = transferRequest.account,
230+
rendezvous = rendezvous.toPublicKey(),
231+
exchangeData = transferExchangeData,
232+
)
233+
}.fold(
220234
onSuccess = {
221-
transactionController.pollIntentMetadata(
222-
owner = sendingVault.authority.keyPair,
223-
intentId = it.id
224-
)
235+
paymentTrace.spanSuspendOrRun("pollIntentMetadata") {
236+
transactionController.pollIntentMetadata<TransactionMetadata.SendPublicPayment>(
237+
owner = sendingVault.authority.keyPair,
238+
intentId = it.id
239+
)
240+
}
225241
},
226242
onFailure = {
227243
logAndFail(it)
228244
}
229245
)
246+
PaymentTraceRegistry.finish(correlationId, success = outcome.isSuccess, error = outcome.exceptionOrNull())
247+
return outcome
230248
}
231249

232250
/** Cancels the coroutine scope and clears all held state. */

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

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import com.getcode.opencode.providers.TokenMetadataProvider
1313
import com.getcode.opencode.model.core.errors.SubmitIntentError
1414
import com.getcode.utils.CodeServerError
1515
import com.getcode.utils.NotifiableError
16-
import com.getcode.utils.timedTraceSuspend
16+
import com.getcode.utils.payment.PaymentTraceRegistry
17+
import com.getcode.utils.payment.spanSuspendOrRun
1718
import kotlinx.coroutines.CoroutineScope
1819
import kotlinx.coroutines.cancel
1920

@@ -102,69 +103,59 @@ internal class GrabBillTransactor(
102103
private suspend fun handleMultiMintScan(
103104
ownerKey: AccountCluster,
104105
data: OpenCodePayload
105-
): Result<TransactionMetadata.SendPublicPayment> = timedTraceSuspend(
106-
message = "handleMultiMintScan",
107-
tag = tag,
108-
) { onStep ->
106+
): Result<TransactionMetadata.SendPublicPayment> {
107+
val trace = PaymentTraceRegistry.get(data.rendezvous.publicKey)
108+
109109
// 1. Wait for the give request from the sender so we can determine what mint we are operating on
110-
val (messageId, giveRequestMint, exchangeData, mintMetadata) = messagingController.pollForGiveRequest(data.rendezvous)
111-
.getOrNull()
112-
?: run {
113-
onStep("pollForGiveRequest")
114-
return@timedTraceSuspend logAndFail(GrabTransactorError.GiveRequestNotFound())
115-
}
116-
onStep("pollForGiveRequest")
110+
val (messageId, giveRequestMint, exchangeData, mintMetadata) = trace.spanSuspendOrRun("pollForGiveRequest") {
111+
messagingController.pollForGiveRequest(data.rendezvous).getOrNull()
112+
} ?: return logAndFail(GrabTransactorError.GiveRequestNotFound())
117113

118114
// 2. Utilize the mint from the give request to get the Token metadata
119-
val token = mintMetadata
120-
?: (tokenProvider.getTokenMetadata(giveRequestMint)
121-
.getOrNull()?.token
122-
?: run {
123-
onStep("tokenMetadata")
124-
return@timedTraceSuspend logAndFail(GrabTransactorError.Other(message = "No token found for proposed mint"))
125-
})
126-
onStep("tokenMetadata")
115+
val token = trace.spanSuspendOrRun("tokenMetadata") {
116+
mintMetadata ?: tokenProvider.getTokenMetadata(giveRequestMint).getOrNull()?.token
117+
} ?: return logAndFail(GrabTransactorError.Other(message = "No token found for proposed mint"))
127118

128119
val tokenizedCluster = ownerKey.withTimelockForToken(token)
129120

130121
// 3. create an account if we don't currently have one for this token
131122
val needsAccount = !accountController.hasAccountFor(token.address)
132123
if (needsAccount) {
133-
accountController.createUserAccount(
134-
ownerForMint = tokenizedCluster,
135-
mint = token.address
136-
).recoverCatching { error ->
137-
if (error is SubmitIntentError.Denied && error.isUnexpectedOwnerAccount) {
138-
// Safety net: PR #660 mitigates the upstream cause (getUserFlags
139-
// failure preventing account bootstrap), but if the core account
140-
// still isn't set up we recover here by triggering the normal
141-
// bootstrap path before retrying.
142-
accountController.refreshAccountState()
143-
// Retry the original non-core mint account
144-
accountController.createUserAccount(
145-
ownerForMint = tokenizedCluster,
146-
mint = token.address
147-
).getOrThrow()
148-
} else {
149-
throw error
124+
val creation = trace.spanSuspendOrRun("createUserAccount", metadata = mapOf("needed" to true)) {
125+
accountController.createUserAccount(
126+
ownerForMint = tokenizedCluster,
127+
mint = token.address
128+
).recoverCatching { error ->
129+
if (error is SubmitIntentError.Denied && error.isUnexpectedOwnerAccount) {
130+
// Safety net: PR #660 mitigates the upstream cause (getUserFlags
131+
// failure preventing account bootstrap), but if the core account
132+
// still isn't set up we recover here by triggering the normal
133+
// bootstrap path before retrying.
134+
accountController.refreshAccountState()
135+
// Retry the original non-core mint account
136+
accountController.createUserAccount(
137+
ownerForMint = tokenizedCluster,
138+
mint = token.address
139+
).getOrThrow()
140+
} else {
141+
throw error
142+
}
150143
}
151-
}.onFailure {
152-
onStep("createUserAccount (needed=true)")
153-
return@timedTraceSuspend handleGrabError(it)
154144
}
145+
creation.exceptionOrNull()?.let { return handleGrabError(it) }
155146
}
156-
onStep("createUserAccount (needed=$needsAccount)")
157147

158148
// 4. Send grab request and wait for confirmation
159-
val result = requestGrab<TransactionMetadata.SendPublicPayment>(tokenizedCluster, data)
160-
.map { it.copy(verifiedExchangeData = exchangeData) }
161-
.onSuccess {
162-
// 5. Ack the receipt of the give request to clear it from the stream
163-
messagingController.ackMessages(data.rendezvous, listOf(messageId))
164-
}
165-
onStep("requestGrab+ack")
149+
val result = trace.spanSuspendOrRun("requestGrab") {
150+
requestGrab<TransactionMetadata.SendPublicPayment>(tokenizedCluster, data)
151+
.map { it.copy(verifiedExchangeData = exchangeData) }
152+
.onSuccess {
153+
// 5. Ack the receipt of the give request to clear it from the stream
154+
messagingController.ackMessages(data.rendezvous, listOf(messageId))
155+
}
156+
}
166157

167-
result
158+
return result
168159
}
169160

170161
private fun handleGrabError(error: Throwable): Result<Nothing> {

0 commit comments

Comments
 (0)