From 12ec416490443ea1ddd47bcd5c27a843f7e81ea8 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 18 May 2026 17:17:12 -0400 Subject: [PATCH 1/2] feat(ocp): add StatelessSwap RPC and consolidate swap errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add StatelessSwap bidirectional streaming RPC — proto definitions, API binding, executor, server parameter/result models, and protobuf mapping extensions. Wire it through TransactionService alongside the existing stateful swap (renamed internally to statefulSwap for clarity). Signed-off-by: Brandon McAnsh --- .../transaction/v1/transaction_service.proto | 174 ++++++++++++++ .../internal/network/api/TransactionApi.kt | 4 + .../executors/StatelessSwapExecutor.kt | 226 ++++++++++++++++++ .../network/extensions/ProtobufToLocal.kt | 26 ++ .../network/services/TransactionService.kt | 35 ++- .../opencode/model/core/errors/Errors.kt | 25 ++ .../model/transactions/StatelessSwapResult.kt | 15 ++ .../StatelessSwapServerParameters.kt | 27 +++ 8 files changed, 527 insertions(+), 5 deletions(-) create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatelessSwapExecutor.kt create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapResult.kt create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapServerParameters.kt diff --git a/definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto b/definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto index f06736d12..c36ebccc8 100644 --- a/definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto +++ b/definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto @@ -81,6 +81,10 @@ service Transaction { // swap is funded. rpc StatefulSwap(stream StatefulSwapRequest) returns (stream StatefulSwapResponse); + // StatelessSwap is like StatefulSwap, but without a state management system and a + // best-effort submission system. + rpc StatelessSwap(stream StatelessSwapRequest) returns (stream StatelessSwapResponse); + // GetSwap gets metadata for a swap rpc GetSwap(GetSwapRequest) returns (GetSwapResponse); @@ -777,6 +781,176 @@ message StatefulSwapResponse { } } +message StatelessSwapRequest { + oneof request { + option (validate.required) = true; + + Initiate initiate = 1; + SubmitSignatures submit_signatures = 2; + } + + message Initiate { + oneof kind { + option (validate.required) = true; + + CoinbaseStableSwapperClientParameters stablecoin = 1; + } + + // Client parameters for stateless swaps via the Coinbase Stable + // Swapper program. Source funds are drawn from the owner's source-mint + // ATA; destination is the owner's destination-mint VM Deposit ATA. + message CoinbaseStableSwapperClientParameters { + // The source mint that will be swapped from. + common.v1.SolanaAccountId from_mint = 1 [(validate.rules).message.required = true]; + + + + // The destination mint that will be swapped to. + common.v1.SolanaAccountId to_mint = 2 [(validate.rules).message.required = true]; + + + + // The amount to swap from the source mint in quarks. + uint64 swap_amount = 3 [(validate.rules).uint64.gt = 0]; + + + } + + // The owner account that owns the source ATA and the destination VM + // Deposit ATA. The owner is the sole client-side signer of the swap + // transaction. + common.v1.SolanaAccountId owner = 2 [(validate.rules).message.required = true]; + + + + // If true, server waits until the swap transaction is finalized before + // returning Success. If false, server returns Success as soon as the + // transaction is submitted to the cluster. + bool wait_for_finalization = 3; + + // The signature is of serialize(StatelessSwapRequest.Initiate) without + // this field set using the private key of the owner account. This + // provides an authentication mechanism to the RPC. + common.v1.Signature signature = 4 [(validate.rules).message.required = true]; + + + } + + message SubmitSignatures { + // The owner's signature over the locally constructed swap transaction. + repeated common.v1.Signature transaction_signatures = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1 + }]; + + + } +} + +message StatelessSwapResponse { + oneof response { + option (validate.required) = true; + + ServerParameters server_parameters = 1; + Success success = 2; + Error error = 3; + } + + message ServerParameters { + oneof kind { + option (validate.required) = true; + + CoinbaseStableSwapperServerParameter stablecoin = 1; + } + + // Server parameters for executing stateless swap flows against the + // Coinbase Stable Swapper program. + // + // Supported Solana transaction version: v0 + // + // Instruction format: + // 1. [Optional] ComputeBudget::SetComputeUnitLimit + // 2. [Optional] ComputeBudget::SetComputeUnitPrice + // 3. [Optional] Memo::Memo + // 4. AssociatedTokenAccount::CreateIdempotent (open owner's to_mint VM Deposit ATA) + // 5. CoinbaseStableSwapper::Swap (owner's from_mint ATA -> owner's to_mint VM Deposit ATA) + message CoinbaseStableSwapperServerParameter { + // Subsidizer account that will pay the transaction fee. + common.v1.SolanaAccountId payer = 1 [(validate.rules).message.required = true]; + + + + // The Solana blockhash to set on the transaction. This is a + // regular recent blockhash, not a durable nonce. + common.v1.Blockhash blockhash = 2 [(validate.rules).message.required = true]; + + + + // ALTs that should be used when constructing the versioned transaction + repeated common.v1.SolanaAddressLookupTable alts = 3; + + // Compute unit limit provided to the ComputeBudget::SetComputeUnitLimit + // instruction. If the value is 0, then the instruction can be omitted. + uint32 compute_unit_limit = 4; + + // Compute unit price provided in the ComputeBudget::SetComputeUnitPrice + // instruction. If the value is 0, then the instruction can be omitted. + uint64 compute_unit_price = 5; + + // Value provided into the Memo::Memo instruction. If the value length is 0, + // then the instruction can be omitted. + string memo_value = 6 [(validate.rules).string.max_len = 64]; + + + + // The CoinbaseStableSwapper liquidity pool's configured fee recipient, + // sourced from the on-chain LiquidityPool account. Required by the + // CoinbaseStableSwapper::Swap instruction. + common.v1.SolanaAccountId pool_fee_recipient = 7 [(validate.rules).message.required = true]; + + + } + } + + message Success { + Code code = 1; + enum Code { + // Transaction was forwarded to the cluster. Returned when + // wait_for_finalization = false. + SUBMITTED = 0; + // Transaction was finalized on-chain. Returned when + // wait_for_finalization = true. + FINALIZED = 1; + } + + // The signature of the submitted swap transaction. Clients may use + // this to look up the transaction on-chain. + common.v1.Signature transaction_signature = 2 [(validate.rules).message.required = true]; + + + } + + message Error { + Code code = 1; + enum Code { + // Denied by a guard (spam, money laundering, etc) + DENIED = 0; + // There is an issue with the provided transaction signature + SIGNATURE_ERROR = 1; + // The swap parameters failed server-side validation (eg. + // unsupported mint pair, insufficient source balance, swap amount + // out of allowed range) + INVALID_SWAP = 2; + // The transaction was submitted but reverted on-chain, or its + // blockhash expired before confirmation. Only relevant when + // wait_for_finalization = true. + TRANSACTION_FAILED = 3; + } + + repeated ErrorDetails error_details = 2; + } +} + message GetSwapRequest { common.v1.SwapId id = 1 [(validate.rules).message.required = true]; diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/TransactionApi.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/TransactionApi.kt index 7707d9fa4..cf3101b53 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/TransactionApi.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/TransactionApi.kt @@ -245,4 +245,8 @@ class TransactionApi @Inject constructor( fun swap( requestFlow: Flow ): Flow = api.statefulSwap(requestFlow) + + fun statelessSwap( + requestFlow: Flow + ): Flow = api.statelessSwap(requestFlow) } \ No newline at end of file diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatelessSwapExecutor.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatelessSwapExecutor.kt new file mode 100644 index 000000000..392b94817 --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatelessSwapExecutor.kt @@ -0,0 +1,226 @@ +package com.getcode.opencode.internal.network.executors + +import com.codeinc.opencode.gen.transaction.v1.TransactionService +import com.getcode.ed25519.Ed25519.KeyPair +import com.getcode.opencode.internal.bidi.BidirectionalStreamReference +import com.getcode.opencode.internal.bidi.openBidirectionalStreamForResult +import com.getcode.opencode.internal.network.api.TransactionApi +import com.getcode.opencode.internal.network.extensions.asSignature +import com.getcode.opencode.internal.network.extensions.asSolanaAccountId +import com.getcode.opencode.internal.network.extensions.sign +import com.getcode.opencode.internal.network.extensions.toCode +import com.getcode.opencode.internal.network.extensions.toProps +import com.getcode.opencode.internal.network.extensions.toSignature +import com.getcode.opencode.model.core.errors.SwapError +import com.getcode.opencode.model.core.errors.SubmitIntentError +import com.getcode.opencode.model.transactions.StatelessSwapResult +import com.getcode.opencode.model.transactions.StatelessSwapServerParameters +import com.getcode.opencode.model.transactions.StatelessSwapSuccess +import com.getcode.opencode.solana.SolanaTransaction +import com.getcode.services.opencode.BuildConfig +import com.getcode.solana.keys.Mint +import com.getcode.solana.keys.Signature +import com.getcode.solana.keys.base58 +import com.getcode.utils.TraceType +import com.getcode.utils.trace +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.coroutines.resume + +private typealias StatelessSwapStreamReference = + BidirectionalStreamReference + +internal class StatelessSwapExecutor( + private val api: TransactionApi, +) { + private val streamReferenceMutex = Mutex() + + suspend fun execute( + scope: CoroutineScope, + owner: KeyPair, + fromMint: Mint, + toMint: Mint, + amount: Long, + waitForFinalization: Boolean = false, + buildAndSign: (StatelessSwapServerParameters) -> Pair>, + ): StatelessSwapResult = suspendCancellableCoroutine { cont -> + trace(tag = TAG, message = "Opening stateless swap stream") + + val streamReference = StatelessSwapStreamReference(scope, "stateless-swap") + streamReference.retain() + + scope.launch { + try { + val result = openStream( + streamRef = streamReference, + owner = owner, + fromMint = fromMint, + toMint = toMint, + amount = amount, + waitForFinalization = waitForFinalization, + buildAndSign = buildAndSign, + ) + cont.resume(result) + } catch (e: Exception) { + trace(tag = TAG, message = "Failed to open stateless swap stream.", error = e) + if (!cont.isCompleted) { + cont.resume(Result.failure(SubmitIntentError.Other(cause = e))) + } + } + } + + cont.invokeOnCancellation { + scope.launch { + runCatching { + streamReferenceMutex.withLock { streamReference.destroy() } + }.onFailure { throwable -> + trace( + tag = TAG, + message = "Cancellation cleanup failed: ${throwable.message}", + type = TraceType.Silent + ) + } + } + } + } + + private suspend fun openStream( + streamRef: StatelessSwapStreamReference, + owner: KeyPair, + fromMint: Mint, + toMint: Mint, + amount: Long, + waitForFinalization: Boolean, + buildAndSign: (StatelessSwapServerParameters) -> Pair>, + ): StatelessSwapResult = openBidirectionalStreamForResult( + streamRef = streamRef, + apiCall = api::statelessSwap, + initialRequest = { + buildInitiateRequest(owner, fromMint, toMint, amount, waitForFinalization) + }, + responseHandler = { response, onResult, requestChannel -> + when (response.responseCase) { + TransactionService.StatelessSwapResponse.ResponseCase.SERVER_PARAMETERS -> { + handleServerParameters( + serverParameters = response.serverParameters, + buildAndSign = buildAndSign, + onResult = onResult, + requestChannel = requestChannel, + ) + } + + TransactionService.StatelessSwapResponse.ResponseCase.SUCCESS -> { + streamRef.complete() + val code = response.success.toCode() + val signature = runCatching { response.success.transactionSignature.toSignature() }.getOrNull() + if (code == null || signature == null) { + trace(tag = TAG, message = "Success but failed to parse response") + onResult(Result.failure(SwapError.Other())) + } else { + trace(tag = TAG, message = "Success: ${response.success.code}") + onResult(Result.success(StatelessSwapSuccess(code, signature))) + } + } + + TransactionService.StatelessSwapResponse.ResponseCase.ERROR -> { + val errors = handleErrors(response.error.errorDetailsList) + trace( + tag = TAG, + message = "Error: (${response.error.code}) ${errors.joinToString("\n")}", + type = TraceType.Error, + ) + streamRef.complete() + onResult(Result.failure(SwapError.typed(response.error))) + } + + TransactionService.StatelessSwapResponse.ResponseCase.RESPONSE_NOT_SET -> Unit + } + } + ) +} + +private fun buildInitiateRequest( + owner: KeyPair, + fromMint: Mint, + toMint: Mint, + amount: Long, + waitForFinalization: Boolean, +): TransactionService.StatelessSwapRequest { + val initiate = TransactionService.StatelessSwapRequest.Initiate.newBuilder() + .setOwner(owner.asSolanaAccountId()) + .setStablecoin( + TransactionService.StatelessSwapRequest.Initiate.CoinbaseStableSwapperClientParameters.newBuilder() + .setFromMint(fromMint.asSolanaAccountId()) + .setToMint(toMint.asSolanaAccountId()) + .setSwapAmount(amount) + .build() + ) + .setWaitForFinalization(waitForFinalization) + .apply { setSignature(sign(owner)) } + .build() + + return TransactionService.StatelessSwapRequest.newBuilder() + .setInitiate(initiate) + .build() +} + +private fun handleServerParameters( + serverParameters: TransactionService.StatelessSwapResponse.ServerParameters?, + buildAndSign: (StatelessSwapServerParameters) -> Pair>, + requestChannel: (TransactionService.StatelessSwapRequest) -> Unit, + onResult: (StatelessSwapResult) -> Unit, +) { + try { + val params = when (serverParameters?.kindCase) { + TransactionService.StatelessSwapResponse.ServerParameters.KindCase.STABLECOIN -> + serverParameters.stablecoin.toProps() + TransactionService.StatelessSwapResponse.ServerParameters.KindCase.KIND_NOT_SET, + null -> null + } + + if (params == null) { + onResult(Result.failure(SwapError.Other(cause = IllegalArgumentException("Invalid server parameters")))) + return + } + + trace(tag = TAG, message = "Received server parameters. Building transaction and submitting signatures...") + + val (_, signatures) = buildAndSign(params) + + val submitSignatures = TransactionService.StatelessSwapRequest.SubmitSignatures.newBuilder() + .addAllTransactionSignatures(signatures.map { it.asSignature() }) + .build() + + requestChannel( + TransactionService.StatelessSwapRequest.newBuilder() + .setSubmitSignatures(submitSignatures) + .build() + ) + } catch (e: Exception) { + if (BuildConfig.DEBUG) e.printStackTrace() + onResult(Result.failure(SwapError.Other(cause = e))) + } +} + +private fun handleErrors( + errorDetails: List +): List { + val errors = mutableListOf() + errorDetails.forEach { error -> + when (error.typeCase) { + TransactionService.ErrorDetails.TypeCase.REASON_STRING -> + errors.add("Reason: ${error.reasonString.reason}") + TransactionService.ErrorDetails.TypeCase.INVALID_SIGNATURE -> + errors.add("Action index: ${error.invalidSignature.actionId}") + TransactionService.ErrorDetails.TypeCase.DENIED -> + errors.add("Denied: ${error.denied.reason}") + else -> Unit + } + } + return errors +} + +private const val TAG = "StatelessSwap" diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt index 40c049169..97276731a 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt @@ -25,6 +25,8 @@ import com.getcode.opencode.model.transactions.AddressLookupTable import com.getcode.opencode.model.transactions.ExchangeData import com.getcode.opencode.model.transactions.SwapFundingSource import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatelessSwapServerParameters +import com.getcode.opencode.model.transactions.StatelessSwapSuccessCode import com.getcode.opencode.model.transactions.SwapSuccessCode import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.model.transactions.VerifiedSwapMetadata @@ -234,6 +236,30 @@ internal fun TransactionService.StatefulSwapResponse.Success.toCode(): SwapSucce } } +internal fun TransactionService.StatelessSwapResponse.ServerParameters.CoinbaseStableSwapperServerParameter.toProps(): StatelessSwapServerParameters { + return StatelessSwapServerParameters( + payer = payer.toPublicKey(), + blockhash = blockhash.toHash(), + alts = altsList.map { table -> + val address = table.address.toPublicKey() + val entries = table.entriesList.map { it.toPublicKey() } + AddressLookupTable(address, entries) + }, + computeUnitLimit = computeUnitLimit, + computeUnitPrice = computeUnitPrice, + memoValue = memoValue, + poolFeeRecipient = poolFeeRecipient.toPublicKey(), + ) +} + +internal fun TransactionService.StatelessSwapResponse.Success.toCode(): StatelessSwapSuccessCode? { + return when (this.code) { + TransactionService.StatelessSwapResponse.Success.Code.SUBMITTED -> StatelessSwapSuccessCode.Submitted + TransactionService.StatelessSwapResponse.Success.Code.FINALIZED -> StatelessSwapSuccessCode.Finalized + TransactionService.StatelessSwapResponse.Success.Code.UNRECOGNIZED -> null + } +} + internal fun CurrencyService.VmMetadata.toMetadata(): VmMetadata { return VmMetadata( vm = vm.toPublicKey(), diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt index a86c84cf2..0ef77cbe4 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt @@ -8,6 +8,7 @@ import com.getcode.opencode.internal.domain.mapping.TransactionMetadataMapper import com.getcode.opencode.internal.manager.VerifiedState import com.getcode.opencode.internal.network.api.TransactionApi import com.getcode.opencode.internal.network.executors.IntentExecutor +import com.getcode.opencode.internal.network.executors.StatelessSwapExecutor import com.getcode.opencode.internal.network.executors.SwapExecutor import com.getcode.opencode.internal.network.extensions.foldWithSuppression import com.getcode.opencode.internal.network.extensions.toModel @@ -18,21 +19,24 @@ import com.getcode.opencode.model.core.errors.GetIntentMetadataError import com.getcode.opencode.model.core.errors.GetLimitsError import com.getcode.opencode.model.core.errors.VoidGiftCardError import com.getcode.opencode.model.core.errors.WithdrawalAvailabilityError -import com.getcode.opencode.model.financial.Fiat import com.getcode.opencode.model.financial.Limits import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.minus +import com.getcode.opencode.model.transactions.StatelessSwapResult +import com.getcode.opencode.model.transactions.StatelessSwapServerParameters import com.getcode.opencode.model.transactions.SwapDirection import com.getcode.opencode.model.transactions.SwapFundingSource import com.getcode.opencode.model.transactions.SwapRequest import com.getcode.opencode.model.transactions.SwapStartKind import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.model.transactions.WithdrawalAvailability +import com.getcode.opencode.solana.SolanaTransaction import com.getcode.opencode.solana.intents.IntentType import com.getcode.opencode.utils.toValidationOrElse import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey +import com.getcode.solana.keys.Signature import com.getcode.solana.keys.base58 import com.getcode.utils.trace import kotlinx.coroutines.CoroutineScope @@ -208,7 +212,7 @@ internal class TransactionService @Inject constructor( ) val fundedWith = fund ?: { swapFunding.fund(scope, owner, it).map { Unit } } - return swap(scope, request, owner, fundedWith) + return statefulSwap(scope, request, owner, fundedWith) } suspend fun sell( @@ -236,7 +240,7 @@ internal class TransactionService @Inject constructor( verifiedState = verifiedState, ) - return swap(scope, request, tokenCluster) + return statefulSwap(scope, request, tokenCluster) } suspend fun withdrawUsdf( @@ -265,10 +269,31 @@ internal class TransactionService @Inject constructor( verifiedState = verifiedState, ) - return swap(scope, request, owner) + return statefulSwap(scope, request, owner) } - private suspend fun swap( + private suspend fun statelessSwap( + scope: CoroutineScope, + owner: AccountCluster, + fromMint: Mint, + toMint: Mint, + amount: Long, + waitForFinalization: Boolean = false, + buildAndSign: (StatelessSwapServerParameters) -> Pair>, + ): StatelessSwapResult { + val executor = StatelessSwapExecutor(api) + return executor.execute( + scope = scope, + owner = owner.authority.keyPair, + fromMint = fromMint, + toMint = toMint, + amount = amount, + waitForFinalization = waitForFinalization, + buildAndSign = buildAndSign, + ) + } + + private suspend fun statefulSwap( scope: CoroutineScope, request: SwapRequest, owner: AccountCluster, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt index c355e312b..88a9a988f 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt @@ -264,6 +264,7 @@ sealed class SwapError( class InvalidSwap(reasons: List): SwapError(message = reasons.joinToString()), NotifiableError { val insufficientBalance = reasons.contains("insufficient balance") } + class TransactionFailed(reasons: List): SwapError(message = reasons.joinToString()), NotifiableError data class Other(override val cause: Throwable? = null) : SwapError(message = cause?.message, cause = cause), NotifiableError @@ -293,6 +294,30 @@ sealed class SwapError( TransactionService.StatefulSwapResponse.Error.Code.INVALID_SWAP -> InvalidSwap(reasonStrings) } } + + fun typed(proto: TransactionService.StatelessSwapResponse.Error): SwapError { + val reasonStrings = proto.errorDetailsList.mapNotNull { + when (it.typeCase) { + TransactionService.ErrorDetails.TypeCase.REASON_STRING -> + it.reasonString.reason.takeIf { reason -> reason.isNotEmpty() } + else -> null + } + } + + return when (proto.code) { + TransactionService.StatelessSwapResponse.Error.Code.DENIED -> { + val reasons = proto.errorDetailsList.mapNotNull { + if (!it.hasDenied()) return@mapNotNull null + it.denied.reason + } + Denied(reasons) + } + TransactionService.StatelessSwapResponse.Error.Code.SIGNATURE_ERROR -> Signature() + TransactionService.StatelessSwapResponse.Error.Code.INVALID_SWAP -> InvalidSwap(reasonStrings) + TransactionService.StatelessSwapResponse.Error.Code.TRANSACTION_FAILED -> TransactionFailed(reasonStrings) + TransactionService.StatelessSwapResponse.Error.Code.UNRECOGNIZED -> Unrecognized() + } + } } } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapResult.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapResult.kt new file mode 100644 index 000000000..6b2af2d90 --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapResult.kt @@ -0,0 +1,15 @@ +package com.getcode.opencode.model.transactions + +import com.getcode.solana.keys.Signature + +typealias StatelessSwapResult = Result + +data class StatelessSwapSuccess( + val code: StatelessSwapSuccessCode, + val transactionSignature: Signature, +) + +enum class StatelessSwapSuccessCode { + Submitted, + Finalized, +} diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapServerParameters.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapServerParameters.kt new file mode 100644 index 000000000..b7864ce76 --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapServerParameters.kt @@ -0,0 +1,27 @@ +package com.getcode.opencode.model.transactions + +import com.getcode.solana.keys.Hash +import com.getcode.solana.keys.PublicKey + +/** + * Server parameters for executing stateless swap flows against the + * Coinbase Stable Swapper program. + * + * Supported Solana transaction version: v0 + * + * Instruction format: + * 1. [Optional] ComputeBudget::SetComputeUnitLimit + * 2. [Optional] ComputeBudget::SetComputeUnitPrice + * 3. [Optional] Memo::Memo + * 4. AssociatedTokenAccount::CreateIdempotent (open owner's to_mint VM Deposit ATA) + * 5. CoinbaseStableSwapper::Swap (owner's from_mint ATA -> owner's to_mint VM Deposit ATA) + */ +data class StatelessSwapServerParameters( + val payer: PublicKey, + val blockhash: Hash, + val alts: List, + val computeUnitLimit: Int, + val computeUnitPrice: Long, + val memoValue: String, + val poolFeeRecipient: PublicKey, +) From c37de6d026539f0b62fc76433c78ab92e37e888f Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 18 May 2026 17:37:09 -0400 Subject: [PATCH 2/2] refactor(ocp): introduce IntentStatelessSwap and rename swap request/response types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add IntentStatelessSwap mirroring IntentSwap pattern — encapsulates initiate and submit-signatures protobuf request building. Update StatelessSwapExecutor to take the intent instead of raw params + lambda. Create StatelessSwapRequest model and rename stateful counterparts: SwapRequest -> StatefulSwapRequest, SwapResponseServerParameters -> StatefulSwapResponseServerParameters, SwapResult -> StatefulSwapResult. Signed-off-by: Brandon McAnsh --- .../getcode/opencode/solana/swap/SwapTests.kt | 6 +- .../controllers/TransactionController.kt | 4 +- .../controllers/TransactionOperations.kt | 4 +- .../InternalTransactionRepository.kt | 4 +- .../{IntentSwap.kt => IntentStatefulSwap.kt} | 26 +++--- .../api/intents/IntentStatelessSwap.kt | 57 +++++++++++++ ...wapExecutor.kt => StatefulSwapExecutor.kt} | 16 ++-- .../executors/StatelessSwapExecutor.kt | 85 +++---------------- .../network/extensions/LocalToProtobuf.kt | 8 +- .../network/extensions/ProtobufToLocal.kt | 16 ++-- .../internal/network/funding/SwapFunding.kt | 4 +- .../network/services/TransactionService.kt | 36 +++----- .../extensions/ExtractServerParameters.kt | 6 +- .../opencode/model/financial/MintMetadata.kt | 2 +- ...{SwapRequest.kt => StatefulSwapRequest.kt} | 2 +- ...> StatefulSwapResponseServerParameters.kt} | 8 +- .../{SwapResult.kt => StatefulSwapResult.kt} | 0 .../transactions/StatelessSwapRequest.kt | 12 +++ .../repositories/TransactionRepository.kt | 4 +- .../opencode/solana/TransactionBuilder.kt | 8 +- .../CoinbaseStablecoinSwapperInstructions.kt | 4 +- .../swap/ExistingCurrencyBuyInstructions.kt | 4 +- .../solana/swap/NewCurrencyBuyInstructions.kt | 5 +- .../opencode/solana/swap/SellInstructions.kt | 4 +- ...inbaseStablecoinSwapperInstructionsTest.kt | 4 +- 25 files changed, 161 insertions(+), 168 deletions(-) rename services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/{IntentSwap.kt => IntentStatefulSwap.kt} (81%) create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatelessSwap.kt rename services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/{SwapExecutor.kt => StatefulSwapExecutor.kt} (96%) rename services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/{SwapRequest.kt => StatefulSwapRequest.kt} (98%) rename services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/{SwapResponseServerParameters.kt => StatefulSwapResponseServerParameters.kt} (98%) rename services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/{SwapResult.kt => StatefulSwapResult.kt} (100%) create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapRequest.kt diff --git a/services/opencode/src/androidTest/kotlin/com/getcode/opencode/solana/swap/SwapTests.kt b/services/opencode/src/androidTest/kotlin/com/getcode/opencode/solana/swap/SwapTests.kt index 65cd5bf8e..a2ee984ea 100644 --- a/services/opencode/src/androidTest/kotlin/com/getcode/opencode/solana/swap/SwapTests.kt +++ b/services/opencode/src/androidTest/kotlin/com/getcode/opencode/solana/swap/SwapTests.kt @@ -23,7 +23,7 @@ import com.getcode.opencode.model.financial.LaunchpadMetadata import com.getcode.opencode.model.financial.MintMetadata import com.getcode.opencode.model.financial.VmMetadata import com.getcode.opencode.model.financial.usdf -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.opencode.tests.generateRandomPublicKeyForTest import com.getcode.opencode.utils.generate import com.getcode.solana.keys.Mint @@ -89,7 +89,7 @@ class SwapInstructionsTest { ) // Mock Server Response (Stateless for simplicity) - private val mockServerParams = SwapResponseServerParameters.ExistingCurrency( + private val mockServerParams = StatefulSwapResponseServerParameters.ExistingCurrency( payer = mockPayer, blockhash = mockRecentBlockhash, nonce = mockNonce, @@ -337,7 +337,7 @@ class SwapInstructionsTest { private val mockNewCurrencyAuthority = generateRandomPublicKeyForTest() private val mockSeed = generateRandomPublicKeyForTest() - private val mockNewCurrencyServerParams = SwapResponseServerParameters.NewCurrency( + private val mockNewCurrencyServerParams = StatefulSwapResponseServerParameters.NewCurrency( payer = mockPayer, blockhash = mockRecentBlockhash, nonce = mockNonce, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionController.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionController.kt index 8857f3806..a04207265 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionController.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionController.kt @@ -24,7 +24,7 @@ import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.transactions.ExchangeData import com.getcode.opencode.model.transactions.SwapFundingSource import com.getcode.opencode.model.transactions.SwapMetadata -import com.getcode.opencode.model.transactions.SwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapRequest import com.getcode.opencode.model.transactions.SwapState import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.model.transactions.WithdrawalAvailability @@ -269,7 +269,7 @@ class TransactionController @Inject constructor( swapId: SwapId?, of: Token, source: SwapFundingSource, - fund: (suspend (SwapRequest) -> Result)?, + fund: (suspend (StatefulSwapRequest) -> Result)?, ): Result { trace("Starting ${amount.localFiat.nativeAmount.formatted()} buy of ${of.symbol}") val tokenizedOwner = owner.withTimelockForToken(of) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionOperations.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionOperations.kt index 8674ebb88..97286e40d 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionOperations.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionOperations.kt @@ -9,7 +9,7 @@ import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.transactions.SwapFundingSource import com.getcode.opencode.model.transactions.SwapMetadata -import com.getcode.opencode.model.transactions.SwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapRequest import com.getcode.opencode.model.transactions.SwapState import com.getcode.opencode.model.transactions.WithdrawalAvailability import com.getcode.opencode.solana.intents.IntentType @@ -35,7 +35,7 @@ interface TransactionOperations { swapId: SwapId? = null, of: Token, source: SwapFundingSource = SwapFundingSource.SubmitIntent(), - fund: (suspend (SwapRequest) -> Result)? = null, + fund: (suspend (StatefulSwapRequest) -> Result)? = null, ): Result suspend fun sell( diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalTransactionRepository.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalTransactionRepository.kt index ada8df51d..084404628 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalTransactionRepository.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalTransactionRepository.kt @@ -9,7 +9,7 @@ import com.getcode.opencode.model.financial.Limits import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.transactions.SwapFundingSource -import com.getcode.opencode.model.transactions.SwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapRequest import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.model.transactions.WithdrawalAvailability import com.getcode.opencode.model.core.errors.SubmitIntentError @@ -73,7 +73,7 @@ internal class InternalTransactionRepository @Inject constructor( swapId: SwapId?, verifiedState: VerifiedState, source: SwapFundingSource, - fund: (suspend (SwapRequest) -> Result)? + fund: (suspend (StatefulSwapRequest) -> Result)? ): Result = service.buy( scope = scope, swapId = swapId, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentSwap.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatefulSwap.kt similarity index 81% rename from services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentSwap.kt rename to services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatefulSwap.kt index d8226cb6c..a96e7bda4 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentSwap.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatefulSwap.kt @@ -10,47 +10,47 @@ import com.getcode.opencode.internal.network.extensions.stablecoinParams import com.getcode.opencode.internal.network.extensions.verifiedMetadata import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.usdf -import com.getcode.opencode.model.transactions.SwapRequest -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.opencode.model.transactions.SwapStartKind import com.getcode.opencode.model.transactions.VerifiedSwapMetadata import com.getcode.opencode.solana.SolanaTransaction import com.getcode.opencode.solana.TransactionBuilder import com.getcode.solana.keys.Signature -internal class IntentSwap( - val request: SwapRequest, +internal class IntentStatefulSwap( + val request: StatefulSwapRequest, val metadata: VerifiedSwapMetadata, ){ - var parameters: SwapResponseServerParameters? = null + var parameters: StatefulSwapResponseServerParameters? = null - fun sign(parameters: SwapResponseServerParameters): List { + fun sign(parameters: StatefulSwapResponseServerParameters): List { val transaction = transaction(parameters) return when (parameters) { - is SwapResponseServerParameters.ExistingCurrency -> { + is StatefulSwapResponseServerParameters.ExistingCurrency -> { transaction.signatures(request.owner.authority.keyPair, request.swapAuthority) } - is SwapResponseServerParameters.NewCurrency -> { + is StatefulSwapResponseServerParameters.NewCurrency -> { // For new currency, owner == swapAuthority, so only 1 unique signature needed transaction.signatures(request.owner.authority.keyPair) } - is SwapResponseServerParameters.Stablecoin -> { + is StatefulSwapResponseServerParameters.Stablecoin -> { transaction.signatures(request.owner.authority.keyPair, request.swapAuthority) } } } - fun transaction(parameters: SwapResponseServerParameters): SolanaTransaction { + fun transaction(parameters: StatefulSwapResponseServerParameters): SolanaTransaction { return when (parameters) { - is SwapResponseServerParameters.ExistingCurrency -> TransactionBuilder.swap( + is StatefulSwapResponseServerParameters.ExistingCurrency -> TransactionBuilder.swap( response = parameters, authority = request.owner.authorityPublicKey, swapAuthority = request.swapAuthority.toPublicKey(), direction = request.direction, amount = request.swapAmount.underlyingTokenAmount.quarks, ) - is SwapResponseServerParameters.NewCurrency -> TransactionBuilder.buyNewCurrency( + is StatefulSwapResponseServerParameters.NewCurrency -> TransactionBuilder.buyNewCurrency( response = parameters, authority = request.owner.authorityPublicKey, coreMintMetadata = Token.usdf, @@ -58,7 +58,7 @@ internal class IntentSwap( feeAmount = request.feeAmount?.underlyingTokenAmount?.quarks, ) - is SwapResponseServerParameters.Stablecoin -> TransactionBuilder.stablecoinSwap( + is StatefulSwapResponseServerParameters.Stablecoin -> TransactionBuilder.stablecoinSwap( response = parameters, authority = request.owner.authorityPublicKey, swapAuthority = request.swapAuthority.toPublicKey(), diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatelessSwap.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatelessSwap.kt new file mode 100644 index 000000000..5d4da8cdc --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatelessSwap.kt @@ -0,0 +1,57 @@ +package com.getcode.opencode.internal.network.api.intents + +import com.codeinc.opencode.gen.transaction.v1.TransactionService +import com.getcode.opencode.internal.network.extensions.asSignature +import com.getcode.opencode.internal.network.extensions.asSolanaAccountId +import com.getcode.opencode.internal.network.extensions.sign +import com.getcode.opencode.model.transactions.StatelessSwapRequest +import com.getcode.opencode.model.transactions.StatelessSwapServerParameters +import com.getcode.opencode.solana.SolanaTransaction +import com.getcode.solana.keys.Signature + +internal class IntentStatelessSwap( + val request: StatelessSwapRequest, +) { + var parameters: StatelessSwapServerParameters? = null + + fun sign(parameters: StatelessSwapServerParameters): List { + val transaction = transaction(parameters) + return transaction.signatures(request.owner.authority.keyPair) + } + + fun transaction(parameters: StatelessSwapServerParameters): SolanaTransaction { + TODO("Stateless swap transaction building not yet implemented") + } + + fun initiate(): TransactionService.StatelessSwapRequest { + val owner = request.owner.authority.keyPair + val initiate = TransactionService.StatelessSwapRequest.Initiate.newBuilder() + .setOwner(owner.asSolanaAccountId()) + .setStablecoin( + TransactionService.StatelessSwapRequest.Initiate.CoinbaseStableSwapperClientParameters.newBuilder() + .setFromMint(request.fromMint.asSolanaAccountId()) + .setToMint(request.toMint.asSolanaAccountId()) + .setSwapAmount(request.amount) + .build() + ) + .setWaitForFinalization(request.waitForFinalization) + .apply { setSignature(sign(owner)) } + .build() + + return TransactionService.StatelessSwapRequest.newBuilder() + .setInitiate(initiate) + .build() + } + + fun requestToSubmitSignatures(): TransactionService.StatelessSwapRequest { + val params = parameters ?: throw IllegalStateException("parameters not set") + + return TransactionService.StatelessSwapRequest.newBuilder() + .setSubmitSignatures( + TransactionService.StatelessSwapRequest.SubmitSignatures.newBuilder() + .addAllTransactionSignatures(sign(params).map { it.asSignature() }) + .build() + ) + .build() + } +} diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/SwapExecutor.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatefulSwapExecutor.kt similarity index 96% rename from services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/SwapExecutor.kt rename to services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatefulSwapExecutor.kt index 4e47d6302..a965da7d9 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/SwapExecutor.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatefulSwapExecutor.kt @@ -4,12 +4,12 @@ import com.codeinc.opencode.gen.transaction.v1.TransactionService import com.getcode.opencode.internal.bidi.BidirectionalStreamReference import com.getcode.opencode.internal.bidi.openBidirectionalStreamForResult import com.getcode.opencode.internal.network.api.TransactionApi -import com.getcode.opencode.internal.network.api.intents.IntentSwap +import com.getcode.opencode.internal.network.api.intents.IntentStatefulSwap import com.getcode.opencode.internal.network.extensions.toCode import com.getcode.opencode.internal.network.extensions.toProps import com.getcode.opencode.model.core.errors.SubmitIntentError import com.getcode.opencode.model.core.errors.SwapError -import com.getcode.opencode.model.transactions.SwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapRequest import com.getcode.opencode.model.transactions.SwapResult import com.getcode.opencode.model.transactions.SwapStartKind import com.getcode.opencode.model.transactions.VerifiedSwapMetadata @@ -27,7 +27,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlin.coroutines.resume -typealias OcpSwapStreamReference = BidirectionalStreamReference +typealias OcpSwapStreamReference = BidirectionalStreamReference /** @@ -49,7 +49,7 @@ internal class SwapExecutor( suspend fun execute( scope: CoroutineScope, - request: SwapRequest, + request: StatefulSwapRequest, ): SwapResult = suspendCancellableCoroutine { cont -> trace( tag = "Swap", @@ -81,7 +81,7 @@ internal class SwapExecutor( ) } - val intent = IntentSwap(request = request, metadata = metadata) + val intent = IntentStatefulSwap(request = request, metadata = metadata) scope.launch { try { @@ -120,7 +120,7 @@ internal class SwapExecutor( private suspend fun openSwapStream( streamRef: OcpSwapStreamReference, - intent: IntentSwap, + intent: IntentStatefulSwap, ): SwapResult = openBidirectionalStreamForResult( streamRef = streamRef, apiCall = api::swap, @@ -172,7 +172,7 @@ internal class SwapExecutor( } private fun handleServerParameters( - intent: IntentSwap, + intent: IntentStatefulSwap, serverParameters: TransactionService.StatefulSwapResponse.ServerParameters?, requestChannel: (TransactionService.StatefulSwapRequest) -> Unit, onResult: (SwapResult) -> Unit, @@ -217,7 +217,7 @@ private fun handleServerParameters( } private fun handleErrors( - intent: IntentSwap, + intent: IntentStatefulSwap, errorDetails: List ): List { val errors = mutableListOf() diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatelessSwapExecutor.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatelessSwapExecutor.kt index 392b94817..173cf06cc 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatelessSwapExecutor.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/executors/StatelessSwapExecutor.kt @@ -1,26 +1,19 @@ package com.getcode.opencode.internal.network.executors import com.codeinc.opencode.gen.transaction.v1.TransactionService -import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.bidi.BidirectionalStreamReference import com.getcode.opencode.internal.bidi.openBidirectionalStreamForResult import com.getcode.opencode.internal.network.api.TransactionApi -import com.getcode.opencode.internal.network.extensions.asSignature -import com.getcode.opencode.internal.network.extensions.asSolanaAccountId -import com.getcode.opencode.internal.network.extensions.sign +import com.getcode.opencode.internal.network.api.intents.IntentStatelessSwap import com.getcode.opencode.internal.network.extensions.toCode import com.getcode.opencode.internal.network.extensions.toProps import com.getcode.opencode.internal.network.extensions.toSignature -import com.getcode.opencode.model.core.errors.SwapError import com.getcode.opencode.model.core.errors.SubmitIntentError +import com.getcode.opencode.model.core.errors.SwapError +import com.getcode.opencode.model.transactions.StatelessSwapRequest import com.getcode.opencode.model.transactions.StatelessSwapResult -import com.getcode.opencode.model.transactions.StatelessSwapServerParameters import com.getcode.opencode.model.transactions.StatelessSwapSuccess -import com.getcode.opencode.solana.SolanaTransaction import com.getcode.services.opencode.BuildConfig -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.Signature -import com.getcode.solana.keys.base58 import com.getcode.utils.TraceType import com.getcode.utils.trace import kotlinx.coroutines.CoroutineScope @@ -40,29 +33,18 @@ internal class StatelessSwapExecutor( suspend fun execute( scope: CoroutineScope, - owner: KeyPair, - fromMint: Mint, - toMint: Mint, - amount: Long, - waitForFinalization: Boolean = false, - buildAndSign: (StatelessSwapServerParameters) -> Pair>, + request: StatelessSwapRequest, ): StatelessSwapResult = suspendCancellableCoroutine { cont -> trace(tag = TAG, message = "Opening stateless swap stream") val streamReference = StatelessSwapStreamReference(scope, "stateless-swap") streamReference.retain() + val intent = IntentStatelessSwap(request = request) + scope.launch { try { - val result = openStream( - streamRef = streamReference, - owner = owner, - fromMint = fromMint, - toMint = toMint, - amount = amount, - waitForFinalization = waitForFinalization, - buildAndSign = buildAndSign, - ) + val result = openStream(streamReference, intent) cont.resume(result) } catch (e: Exception) { trace(tag = TAG, message = "Failed to open stateless swap stream.", error = e) @@ -89,24 +71,17 @@ internal class StatelessSwapExecutor( private suspend fun openStream( streamRef: StatelessSwapStreamReference, - owner: KeyPair, - fromMint: Mint, - toMint: Mint, - amount: Long, - waitForFinalization: Boolean, - buildAndSign: (StatelessSwapServerParameters) -> Pair>, + intent: IntentStatelessSwap, ): StatelessSwapResult = openBidirectionalStreamForResult( streamRef = streamRef, apiCall = api::statelessSwap, - initialRequest = { - buildInitiateRequest(owner, fromMint, toMint, amount, waitForFinalization) - }, + initialRequest = { intent.initiate() }, responseHandler = { response, onResult, requestChannel -> when (response.responseCase) { TransactionService.StatelessSwapResponse.ResponseCase.SERVER_PARAMETERS -> { handleServerParameters( + intent = intent, serverParameters = response.serverParameters, - buildAndSign = buildAndSign, onResult = onResult, requestChannel = requestChannel, ) @@ -142,34 +117,9 @@ internal class StatelessSwapExecutor( ) } -private fun buildInitiateRequest( - owner: KeyPair, - fromMint: Mint, - toMint: Mint, - amount: Long, - waitForFinalization: Boolean, -): TransactionService.StatelessSwapRequest { - val initiate = TransactionService.StatelessSwapRequest.Initiate.newBuilder() - .setOwner(owner.asSolanaAccountId()) - .setStablecoin( - TransactionService.StatelessSwapRequest.Initiate.CoinbaseStableSwapperClientParameters.newBuilder() - .setFromMint(fromMint.asSolanaAccountId()) - .setToMint(toMint.asSolanaAccountId()) - .setSwapAmount(amount) - .build() - ) - .setWaitForFinalization(waitForFinalization) - .apply { setSignature(sign(owner)) } - .build() - - return TransactionService.StatelessSwapRequest.newBuilder() - .setInitiate(initiate) - .build() -} - private fun handleServerParameters( + intent: IntentStatelessSwap, serverParameters: TransactionService.StatelessSwapResponse.ServerParameters?, - buildAndSign: (StatelessSwapServerParameters) -> Pair>, requestChannel: (TransactionService.StatelessSwapRequest) -> Unit, onResult: (StatelessSwapResult) -> Unit, ) { @@ -188,17 +138,8 @@ private fun handleServerParameters( trace(tag = TAG, message = "Received server parameters. Building transaction and submitting signatures...") - val (_, signatures) = buildAndSign(params) - - val submitSignatures = TransactionService.StatelessSwapRequest.SubmitSignatures.newBuilder() - .addAllTransactionSignatures(signatures.map { it.asSignature() }) - .build() - - requestChannel( - TransactionService.StatelessSwapRequest.newBuilder() - .setSubmitSignatures(submitSignatures) - .build() - ) + intent.parameters = params + requestChannel(intent.requestToSubmitSignatures()) } catch (e: Exception) { if (BuildConfig.DEBUG) e.printStackTrace() onResult(Result.failure(SwapError.Other(cause = e))) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt index cbe474f4f..804914184 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt @@ -20,7 +20,7 @@ import com.getcode.opencode.model.transactions.ExchangeData import com.getcode.opencode.model.transactions.GiveRequest import com.getcode.opencode.model.transactions.GrabRequest import com.getcode.opencode.model.transactions.SwapFundingSource -import com.getcode.opencode.model.transactions.SwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapRequest import com.getcode.opencode.model.transactions.SwapStartKind import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.model.transactions.TransferRequest @@ -275,7 +275,7 @@ internal fun LocalFiat.asExchangeData(): TransactionService.ExchangeData { .build() } -internal fun SwapRequest.currencyCreatorParams(): TransactionService.StatefulSwapRequest.Initiate.ReserveSwapClientParameters.Builder { +internal fun StatefulSwapRequest.currencyCreatorParams(): TransactionService.StatefulSwapRequest.Initiate.ReserveSwapClientParameters.Builder { return when (val details = kind) { is SwapStartKind.Reserve -> { TransactionService.StatefulSwapRequest.Initiate.ReserveSwapClientParameters.newBuilder() @@ -312,7 +312,7 @@ internal fun SwapRequest.currencyCreatorParams(): TransactionService.StatefulSwa } } -internal fun SwapRequest.stablecoinParams(): TransactionService.StatefulSwapRequest.Initiate.CoinbaseStableSwapperClientParameters.Builder { +internal fun StatefulSwapRequest.stablecoinParams(): TransactionService.StatefulSwapRequest.Initiate.CoinbaseStableSwapperClientParameters.Builder { return when (val details = kind) { is SwapStartKind.Reserve -> { throw IllegalStateException("Reserve should not be used for stable swapper params") @@ -332,7 +332,7 @@ internal fun SwapRequest.stablecoinParams(): TransactionService.StatefulSwapRequ } } -internal fun SwapRequest.verifiedMetadata(): TransactionService.VerifiedSwapMetadata.Builder { +internal fun StatefulSwapRequest.verifiedMetadata(): TransactionService.VerifiedSwapMetadata.Builder { return TransactionService.VerifiedSwapMetadata.newBuilder() .apply { when (kind) { diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt index 97276731a..55b9919ba 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt @@ -6,8 +6,6 @@ import com.codeinc.opencode.gen.messaging.v1.MessagingService import com.codeinc.opencode.gen.transaction.v1.TransactionService import com.codeinc.opencode.gen.transaction.v1.clientExchangeDataOrNull import com.codeinc.opencode.gen.transaction.v1.destinationOrNull -import com.codeinc.opencode.gen.transaction.v1.feeDestinationOrNull -import com.codeinc.opencode.gen.transaction.v1.poolFeeRecipientOrNull import com.getcode.opencode.internal.extensions.toHash import com.getcode.opencode.internal.extensions.toMint import com.getcode.opencode.internal.extensions.toPublicKey @@ -24,7 +22,7 @@ import com.getcode.opencode.model.messaging.MessageKind import com.getcode.opencode.model.transactions.AddressLookupTable import com.getcode.opencode.model.transactions.ExchangeData import com.getcode.opencode.model.transactions.SwapFundingSource -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.opencode.model.transactions.StatelessSwapServerParameters import com.getcode.opencode.model.transactions.StatelessSwapSuccessCode import com.getcode.opencode.model.transactions.SwapSuccessCode @@ -133,8 +131,8 @@ internal fun TransactionService.Metadata.toMetadata(): TransactionMetadata { } } -internal fun TransactionService.StatefulSwapResponse.ServerParameters.ReserveExistingCurrencyServerParameters.toProps(): SwapResponseServerParameters { - return SwapResponseServerParameters.ExistingCurrency( +internal fun TransactionService.StatefulSwapResponse.ServerParameters.ReserveExistingCurrencyServerParameters.toProps(): StatefulSwapResponseServerParameters { + return StatefulSwapResponseServerParameters.ExistingCurrency( payer = payer.toPublicKey(), nonce = nonce.toPublicKey(), blockhash = blockhash.toPublicKey(), @@ -151,8 +149,8 @@ internal fun TransactionService.StatefulSwapResponse.ServerParameters.ReserveExi ) } -internal fun TransactionService.StatefulSwapResponse.ServerParameters.ReserveNewCurrencyServerParameter.toProps(): SwapResponseServerParameters { - return SwapResponseServerParameters.NewCurrency( +internal fun TransactionService.StatefulSwapResponse.ServerParameters.ReserveNewCurrencyServerParameter.toProps(): StatefulSwapResponseServerParameters { + return StatefulSwapResponseServerParameters.NewCurrency( payer = payer.toPublicKey(), nonce = nonce.toPublicKey(), blockhash = blockhash.toPublicKey(), @@ -174,8 +172,8 @@ internal fun TransactionService.StatefulSwapResponse.ServerParameters.ReserveNew ) } -internal fun TransactionService.StatefulSwapResponse.ServerParameters.CoinbaseStableSwapperServerParameter.toProps(): SwapResponseServerParameters { - return SwapResponseServerParameters.Stablecoin( +internal fun TransactionService.StatefulSwapResponse.ServerParameters.CoinbaseStableSwapperServerParameter.toProps(): StatefulSwapResponseServerParameters { + return StatefulSwapResponseServerParameters.Stablecoin( payer = payer.toPublicKey(), nonce = nonce.toPublicKey(), blockhash = blockhash.toPublicKey(), diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/funding/SwapFunding.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/funding/SwapFunding.kt index 242c2385d..4fb11533e 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/funding/SwapFunding.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/funding/SwapFunding.kt @@ -6,7 +6,7 @@ import com.getcode.opencode.internal.network.executors.IntentExecutor import com.getcode.opencode.model.accounts.AccountCluster import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.plus -import com.getcode.opencode.model.transactions.SwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapRequest import com.getcode.opencode.solana.intents.IntentType import com.getcode.solana.keys.PublicKey import kotlinx.coroutines.CoroutineScope @@ -18,7 +18,7 @@ internal class SwapFunding @Inject constructor( suspend fun fund( scope: CoroutineScope, owner: AccountCluster, - request: SwapRequest, + request: StatefulSwapRequest, ): Result { val fundingIntent = IntentFundSwap.create( intentId = PublicKey(request.fundingIntentId), diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt index 0ef77cbe4..ab5ec9371 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt @@ -8,6 +8,7 @@ import com.getcode.opencode.internal.domain.mapping.TransactionMetadataMapper import com.getcode.opencode.internal.manager.VerifiedState import com.getcode.opencode.internal.network.api.TransactionApi import com.getcode.opencode.internal.network.executors.IntentExecutor +import com.getcode.opencode.internal.network.api.intents.IntentStatelessSwap import com.getcode.opencode.internal.network.executors.StatelessSwapExecutor import com.getcode.opencode.internal.network.executors.SwapExecutor import com.getcode.opencode.internal.network.extensions.foldWithSuppression @@ -23,20 +24,18 @@ import com.getcode.opencode.model.financial.Limits import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.minus +import com.getcode.opencode.model.transactions.StatelessSwapRequest import com.getcode.opencode.model.transactions.StatelessSwapResult -import com.getcode.opencode.model.transactions.StatelessSwapServerParameters import com.getcode.opencode.model.transactions.SwapDirection import com.getcode.opencode.model.transactions.SwapFundingSource -import com.getcode.opencode.model.transactions.SwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapRequest import com.getcode.opencode.model.transactions.SwapStartKind import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.model.transactions.WithdrawalAvailability -import com.getcode.opencode.solana.SolanaTransaction import com.getcode.opencode.solana.intents.IntentType import com.getcode.opencode.utils.toValidationOrElse import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature import com.getcode.solana.keys.base58 import com.getcode.utils.trace import kotlinx.coroutines.CoroutineScope @@ -182,7 +181,7 @@ internal class TransactionService @Inject constructor( owner: AccountCluster, verifiedState: VerifiedState, source: SwapFundingSource = SwapFundingSource.SubmitIntent(), - fund: (suspend (SwapRequest) -> Result)?, + fund: (suspend (StatefulSwapRequest) -> Result)?, ): Result { // For a freshly-launched stub Token (launchpadMetadata == null && address != USDF), the // Solana program requires authority == buyer == swapAuthority for the atomic 11-instruction @@ -196,7 +195,7 @@ internal class TransactionService @Inject constructor( if (isFreshlyLaunchedStub) owner.authority.keyPair else Ed25519.createKeyPair() val netAmount = amount - (feeAmount ?: LocalFiat.Zero) - val request = SwapRequest( + val request = StatefulSwapRequest( owner = owner, swapAuthority = swapAuthority, kind = SwapStartKind.Reserve( @@ -225,7 +224,7 @@ internal class TransactionService @Inject constructor( ): Result { val tokenCluster = owner.withTimelockForToken(of) - val request = SwapRequest( + val request = StatefulSwapRequest( owner = tokenCluster, swapId = SwapId.generate(), swapAuthority = Ed25519.createKeyPair(), @@ -253,7 +252,7 @@ internal class TransactionService @Inject constructor( ): Result { val netAmount = amount - (fee ?: LocalFiat.Zero) - val request = SwapRequest( + val request = StatefulSwapRequest( owner = owner, swapId = SwapId.generate(), swapAuthority = Ed25519.createKeyPair(), @@ -274,30 +273,17 @@ internal class TransactionService @Inject constructor( private suspend fun statelessSwap( scope: CoroutineScope, - owner: AccountCluster, - fromMint: Mint, - toMint: Mint, - amount: Long, - waitForFinalization: Boolean = false, - buildAndSign: (StatelessSwapServerParameters) -> Pair>, + request: StatelessSwapRequest, ): StatelessSwapResult { val executor = StatelessSwapExecutor(api) - return executor.execute( - scope = scope, - owner = owner.authority.keyPair, - fromMint = fromMint, - toMint = toMint, - amount = amount, - waitForFinalization = waitForFinalization, - buildAndSign = buildAndSign, - ) + return executor.execute(scope, request) } private suspend fun statefulSwap( scope: CoroutineScope, - request: SwapRequest, + request: StatefulSwapRequest, owner: AccountCluster, - fund: suspend (SwapRequest) -> Result = { swapFunding.fund(scope, owner, it).map { Unit } }, + fund: suspend (StatefulSwapRequest) -> Result = { swapFunding.fund(scope, owner, it).map { Unit } }, ): Result { val executor = SwapExecutor(api) return executor.execute(scope, request) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/ExtractServerParameters.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/ExtractServerParameters.kt index c3c749908..fee865494 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/ExtractServerParameters.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/ExtractServerParameters.kt @@ -1,7 +1,7 @@ package com.getcode.opencode.internal.solana.extensions import com.getcode.opencode.model.transactions.AddressLookupTable -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.solana.keys.PublicKey sealed interface ExtractedServerParams { @@ -37,7 +37,7 @@ sealed interface ExtractedServerParams { ): ExtractedServerParams } -internal fun extractServerParameters(serverParameters: SwapResponseServerParameters.ExistingCurrency): ExtractedServerParams.ExistingCurrency { +internal fun extractServerParameters(serverParameters: StatefulSwapResponseServerParameters.ExistingCurrency): ExtractedServerParams.ExistingCurrency { return ExtractedServerParams.ExistingCurrency( payer = serverParameters.payer, alts = serverParameters.alts, @@ -49,7 +49,7 @@ internal fun extractServerParameters(serverParameters: SwapResponseServerParamet ) } -internal fun extractServerParameters(serverParameters: SwapResponseServerParameters.NewCurrency): ExtractedServerParams.NewCurrency { +internal fun extractServerParameters(serverParameters: StatefulSwapResponseServerParameters.NewCurrency): ExtractedServerParams.NewCurrency { return ExtractedServerParams.NewCurrency( payer = serverParameters.payer, alts = serverParameters.alts, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/MintMetadata.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/MintMetadata.kt index 94ac3a9bb..ff95d5b2f 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/MintMetadata.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/MintMetadata.kt @@ -105,7 +105,7 @@ val MintMetadata.Companion.usdc: Token * * All on-chain values that actually drive transaction bytes (sellFeeBps, * vmLockDurationInDays) still come from - * [com.getcode.opencode.model.transactions.SwapResponseServerParameters.NewCurrency] inside + * [com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters.NewCurrency] inside * `buildNewCurrencyBuyInstructions` — this stub never drives those. */ fun MintMetadata.Companion.fromLaunch( diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatefulSwapRequest.kt similarity index 98% rename from services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt rename to services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatefulSwapRequest.kt index 56e171f6a..9d1474115 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapRequest.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatefulSwapRequest.kt @@ -8,7 +8,7 @@ import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.plus import com.getcode.solana.keys.PublicKey -data class SwapRequest( +data class StatefulSwapRequest( val owner: AccountCluster, val swapAuthority: KeyPair, val kind: SwapStartKind, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapResponseServerParameters.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatefulSwapResponseServerParameters.kt similarity index 98% rename from services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapResponseServerParameters.kt rename to services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatefulSwapResponseServerParameters.kt index 2a442d17f..5bb818d8d 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapResponseServerParameters.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatefulSwapResponseServerParameters.kt @@ -2,7 +2,7 @@ package com.getcode.opencode.model.transactions import com.getcode.solana.keys.PublicKey -sealed interface SwapResponseServerParameters { +sealed interface StatefulSwapResponseServerParameters { /** * Subisdizer account that will be paying for the swap */ @@ -120,7 +120,7 @@ sealed interface SwapResponseServerParameters { * The memory index where the destination virtual Timelock account lives */ val memoryIndex: Int, - ): SwapResponseServerParameters + ): StatefulSwapResponseServerParameters /** * Server parameters when executing stateful buy flows against the @@ -204,7 +204,7 @@ sealed interface SwapResponseServerParameters { * Destination account where fee should be paid */ val feeDestination: PublicKey, - ): SwapResponseServerParameters + ): StatefulSwapResponseServerParameters /** * Server parameters when executing stateful swap flows against the @@ -263,5 +263,5 @@ sealed interface SwapResponseServerParameters { * CoinbaseStableSwapper::Swap instruction. */ val poolFeeRecipient: PublicKey, - ): SwapResponseServerParameters + ): StatefulSwapResponseServerParameters } \ No newline at end of file diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapResult.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatefulSwapResult.kt similarity index 100% rename from services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/SwapResult.kt rename to services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatefulSwapResult.kt diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapRequest.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapRequest.kt new file mode 100644 index 000000000..e76c5149e --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/StatelessSwapRequest.kt @@ -0,0 +1,12 @@ +package com.getcode.opencode.model.transactions + +import com.getcode.opencode.model.accounts.AccountCluster +import com.getcode.solana.keys.Mint + +data class StatelessSwapRequest( + val owner: AccountCluster, + val fromMint: Mint, + val toMint: Mint, + val amount: Long, + val waitForFinalization: Boolean = false, +) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/repositories/TransactionRepository.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/repositories/TransactionRepository.kt index b706a7500..d98e841ad 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/repositories/TransactionRepository.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/repositories/TransactionRepository.kt @@ -8,7 +8,7 @@ import com.getcode.opencode.model.financial.Limits import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.transactions.SwapFundingSource -import com.getcode.opencode.model.transactions.SwapRequest +import com.getcode.opencode.model.transactions.StatefulSwapRequest import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.model.transactions.WithdrawalAvailability import com.getcode.opencode.solana.intents.IntentType @@ -54,7 +54,7 @@ interface TransactionRepository { swapId: SwapId? = null, verifiedState: VerifiedState, source: SwapFundingSource = SwapFundingSource.SubmitIntent(), - fund: (suspend (SwapRequest) -> Result)? = null, + fund: (suspend (StatefulSwapRequest) -> Result)? = null, ): Result suspend fun sell( diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/TransactionBuilder.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/TransactionBuilder.kt index 93a03328c..2918da190 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/TransactionBuilder.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/TransactionBuilder.kt @@ -4,7 +4,7 @@ import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.usdf import com.getcode.opencode.model.transactions.SwapDirection -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.opencode.model.financial.MintMetadata import com.getcode.opencode.model.transactions.FundSwapPool import com.getcode.opencode.solana.swap.buildExistingCurrencyBuyInstructions @@ -34,7 +34,7 @@ object TransactionBuilder { * @return A constructed [SolanaTransaction] (V0) ready to be signed and submitted to the network. */ fun swap( - response: SwapResponseServerParameters.ExistingCurrency, + response: StatefulSwapResponseServerParameters.ExistingCurrency, authority: PublicKey, swapAuthority: PublicKey, direction: SwapDirection, @@ -97,7 +97,7 @@ object TransactionBuilder { * @return A constructed [SolanaTransaction] (V0) ready to be signed and submitted to the network. */ fun stablecoinSwap( - response: SwapResponseServerParameters.Stablecoin, + response: StatefulSwapResponseServerParameters.Stablecoin, authority: PublicKey, swapAuthority: PublicKey, destinationOwner: PublicKey, @@ -144,7 +144,7 @@ object TransactionBuilder { * @return A constructed [SolanaTransaction] (V0) ready to be signed and submitted to the network. */ fun buyNewCurrency( - response: SwapResponseServerParameters.NewCurrency, + response: StatefulSwapResponseServerParameters.NewCurrency, authority: PublicKey, coreMintMetadata: MintMetadata, amount: Long, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructions.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructions.kt index b9d8d9d23..3d78a6173 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructions.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructions.kt @@ -11,7 +11,7 @@ import com.getcode.opencode.internal.solana.programs.SystemProgram_AdvanceNonce import com.getcode.opencode.internal.solana.programs.TokenProgram_CloseAccount import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram_TransferForSwapWithFee import com.getcode.opencode.model.financial.MintMetadata -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.opencode.solana.Instruction import com.getcode.solana.keys.PublicKey @@ -44,7 +44,7 @@ import com.getcode.solana.keys.PublicKey * @return A list of [Instruction]s to execute the stablecoin swap. */ internal fun buildStablecoinSwapperInstructions( - serverParameters: SwapResponseServerParameters.Stablecoin, + serverParameters: StatefulSwapResponseServerParameters.Stablecoin, nonce: PublicKey, authority: PublicKey, swapAuthority: PublicKey, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/ExistingCurrencyBuyInstructions.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/ExistingCurrencyBuyInstructions.kt index 6dfb955b4..5f2ba2624 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/ExistingCurrencyBuyInstructions.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/ExistingCurrencyBuyInstructions.kt @@ -12,7 +12,7 @@ import com.getcode.opencode.internal.solana.programs.TokenProgram_CloseAccount import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram_CloseSwapAccountIfEmpty import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram_TransferForSwap import com.getcode.opencode.model.financial.MintMetadata -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.opencode.solana.Instruction import com.getcode.solana.keys.PublicKey @@ -40,7 +40,7 @@ import com.getcode.solana.keys.PublicKey * @return A list of [Instruction]s to execute the buy operation. */ internal fun buildExistingCurrencyBuyInstructions( - serverParameters: SwapResponseServerParameters.ExistingCurrency, + serverParameters: StatefulSwapResponseServerParameters.ExistingCurrency, nonce: PublicKey, authority: PublicKey, swapAuthority: PublicKey, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/NewCurrencyBuyInstructions.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/NewCurrencyBuyInstructions.kt index 489608e09..5197a8389 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/NewCurrencyBuyInstructions.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/NewCurrencyBuyInstructions.kt @@ -18,10 +18,9 @@ import com.getcode.opencode.internal.solana.programs.CurrencyCreatorProgram_Init import com.getcode.opencode.internal.solana.programs.SystemProgram_AdvanceNonce import com.getcode.opencode.internal.solana.programs.TokenProgram_CloseAccount import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram_InitVm -import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram_TransferForSwap import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram_TransferForSwapWithFee import com.getcode.opencode.model.financial.MintMetadata -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.opencode.solana.Instruction import com.getcode.solana.keys.PublicKey @@ -48,7 +47,7 @@ import com.getcode.solana.keys.PublicKey * @return A list of [Instruction]s to execute the new currency creation and initial buy. */ internal fun buildNewCurrencyBuyInstructions( - serverParameters: SwapResponseServerParameters.NewCurrency, + serverParameters: StatefulSwapResponseServerParameters.NewCurrency, nonce: PublicKey, authority: PublicKey, coreMintMetadata: MintMetadata, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/SellInstructions.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/SellInstructions.kt index 9ba157ef1..b26391b5c 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/SellInstructions.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/SellInstructions.kt @@ -12,7 +12,7 @@ import com.getcode.opencode.internal.solana.programs.TokenProgram_CloseAccount import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram_CloseSwapAccountIfEmpty import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram_TransferForSwap import com.getcode.opencode.model.financial.MintMetadata -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.opencode.solana.Instruction import com.getcode.solana.keys.PublicKey @@ -40,7 +40,7 @@ import com.getcode.solana.keys.PublicKey * @return A list of [Instruction]s to execute the sell operation. */ internal fun buildSellInstructions( - serverParameters: SwapResponseServerParameters.ExistingCurrency, + serverParameters: StatefulSwapResponseServerParameters.ExistingCurrency, nonce: PublicKey, authority: PublicKey, swapAuthority: PublicKey, diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructionsTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructionsTest.kt index 108e34720..00896e1d9 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructionsTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructionsTest.kt @@ -15,7 +15,7 @@ import com.getcode.opencode.internal.solana.programs.VirtualMachineProgram import com.getcode.opencode.model.financial.HolderMetrics import com.getcode.opencode.model.financial.MintMetadata import com.getcode.opencode.model.financial.VmMetadata -import com.getcode.opencode.model.transactions.SwapResponseServerParameters +import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey import kotlin.test.Test @@ -73,7 +73,7 @@ class CoinbaseStablecoinSwapperInstructionsTest { private val fromMintMetadata = mintMetadata(fromMintAddress, fromVmAuthority, fromVm) private val toMintMetadata = mintMetadata(toMintAddress, toVmAuthority, toVm) - private val serverParameters = SwapResponseServerParameters.Stablecoin( + private val serverParameters = StatefulSwapResponseServerParameters.Stablecoin( payer = payer, nonce = nonce, blockhash = blockhash,