From 03b688d487712b6b49602753c0cc80fa7964f7ac Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 2 Jun 2026 09:27:30 -0400 Subject: [PATCH] feat(oc): add directTransfer for peer-to-peer sends New `directTransfer()` suspend function on `TransactionController` and new `IntentTransfer.create()` overload accepting `VerifiedState` + `destinationOwner`. Signed-off-by: Brandon McAnsh --- .../com/getcode/opencode/ControllerFactory.kt | 1 - .../controllers/TransactionController.kt | 33 ++++++++++++- .../network/api/intents/IntentTransfer.kt | 48 +++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/ControllerFactory.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/ControllerFactory.kt index 3afddcb10..41b41038f 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/ControllerFactory.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/ControllerFactory.kt @@ -35,7 +35,6 @@ object ControllerFactory { repository = RepositoryFactory.createTransactionRepository(context, config), swapRepository = RepositoryFactory.createSwapRepository(context, config), accountController = createAccountController(context, config), - eventBus = module.providesEventBus(), ) } fun createCurrencyController(context: Context, config: ProtocolConfig): CurrencyController { 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 870edced7..d3ff451e3 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 @@ -7,6 +7,7 @@ import com.getcode.opencode.internal.network.api.intents.IntentDistribution import com.getcode.opencode.internal.network.api.intents.IntentRemoteReceive import com.getcode.opencode.internal.network.api.intents.IntentRemoteSend import com.getcode.opencode.internal.network.api.intents.IntentTransfer +import com.getcode.opencode.internal.solana.extensions.newInstance import com.getcode.opencode.internal.network.api.intents.IntentWithdraw import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.opencode.model.accounts.AccountCluster @@ -20,6 +21,7 @@ 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.solana.keys.TimelockDerivedAccounts import com.getcode.opencode.model.transactions.ExchangeData import com.getcode.opencode.model.transactions.SwapFundingSource import com.getcode.opencode.model.transactions.SwapMetadata @@ -38,7 +40,6 @@ import com.getcode.utils.TraceType import com.getcode.utils.base64 import com.getcode.utils.trace import com.getcode.vendor.Base58 -import com.hoc081098.channeleventbus.ChannelEventBus import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -63,7 +64,6 @@ class TransactionController @Inject constructor( private val repository: TransactionRepository, private val swapRepository: SwapRepository, private val accountController: AccountController, - private val eventBus: ChannelEventBus, ) : TransactionOperations { val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -122,6 +122,35 @@ class TransactionController @Inject constructor( return submitIntent(scope, intent, source.authority.keyPair) } + /** + * Sends funds directly to another user's vault by resolving their + * [destinationOwner] into a timelock-derived vault address. + */ + suspend fun directTransfer( + amount: VerifiedFiat, + token: Token, + source: AccountCluster, + destinationOwner: PublicKey, + scope: CoroutineScope = this.scope, + ): Result { + val verifiedState = amount.verifiedState + ?: return Result.failure(IllegalStateException("No verified state")) + + val timelock = TimelockDerivedAccounts.newInstance(owner = destinationOwner, token = token) + val destinationVault = timelock.vault.publicKey + + val intent = IntentTransfer.create( + amount = amount.localFiat, + mint = token.address, + sourceCluster = source, + destination = destinationVault, + destinationOwner = destinationOwner, + verifiedState = verifiedState, + ) + + return submitIntent(scope, intent, source.authority.keyPair) + } + override suspend fun withdraw( amount: VerifiedFiat, mint: Mint, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentTransfer.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentTransfer.kt index de442d870..5cbbc6a98 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentTransfer.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentTransfer.kt @@ -1,6 +1,7 @@ package com.getcode.opencode.internal.network.api.intents import com.codeinc.opencode.gen.transaction.v1.TransactionService +import com.getcode.opencode.internal.manager.VerifiedState import com.getcode.opencode.internal.network.api.intents.actions.ActionPublicTransfer import com.getcode.opencode.model.accounts.AccountCluster import com.getcode.opencode.internal.network.extensions.asProtobufMetadata @@ -9,9 +10,20 @@ import com.getcode.opencode.model.transactions.ExchangeData import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.solana.intents.ActionGroup import com.getcode.opencode.solana.intents.IntentType +import com.getcode.opencode.utils.generate import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey +/** + * A SendPublicPayment intent for transferring funds to another account. + * + * Two creation paths are supported: + * - **Rendezvous-based** ([create] with [ExchangeData.Verified]): used for + * peer-to-peer bill gives where the rendezvous key serves as the intent id. + * - **Direct** ([create] with [VerifiedState]): used for direct sends to a + * known recipient (e.g. Flipcash contact). A random intent id is generated + * and the [destinationOwner] is included in the metadata. + */ internal class IntentTransfer( override val id: PublicKey, override val metadata: TransactionMetadata, @@ -22,6 +34,7 @@ internal class IntentTransfer( } companion object { + /** Creates a rendezvous-based transfer for peer-to-peer bill gives. */ fun create( amount: LocalFiat, mint: Mint, @@ -54,5 +67,40 @@ internal class IntentTransfer( } ) } + + /** Creates a direct transfer to a known recipient's vault. */ + fun create( + amount: LocalFiat, + mint: Mint, + sourceCluster: AccountCluster, + destination: PublicKey, + destinationOwner: PublicKey, + verifiedState: VerifiedState, + ): IntentTransfer { + val transfer = ActionPublicTransfer.newInstance( + owner = sourceCluster.authority.keyPair, + source = sourceCluster.vaultPublicKey, + destination = destination, + amount = amount.underlyingTokenAmount, + mint = mint, + ) + + return IntentTransfer( + id = PublicKey.generate(), + metadata = TransactionMetadata.SendPublicPayment( + source = sourceCluster.vaultPublicKey, + destination = destination, + destinationOwner = destinationOwner, + amount = amount, + verifiedState = verifiedState, + mint = mint, + isRemoteSend = false, + isWithdrawal = false, + ), + actionGroup = ActionGroup().apply { + actions = listOf(transfer) + }, + ) + } } } \ No newline at end of file