diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80295a2..e2bdf8d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] arrow = "2.2.1" bitcoinj = "0.17" -domainApi = "0.1.1" +domainApi = "0.3.0" flyway = "11.20.2" guice = "7.0.0" hikari = "7.0.2" diff --git a/outie/src/main/kotlin/xyz/block/bittycity/outie/controllers/DomainControllerModule.kt b/outie/src/main/kotlin/xyz/block/bittycity/outie/controllers/DomainControllerModule.kt index f87239d..6472b3e 100644 --- a/outie/src/main/kotlin/xyz/block/bittycity/outie/controllers/DomainControllerModule.kt +++ b/outie/src/main/kotlin/xyz/block/bittycity/outie/controllers/DomainControllerModule.kt @@ -25,6 +25,7 @@ import xyz.block.bittycity.outie.models.Withdrawal import xyz.block.bittycity.outie.models.WithdrawalState import xyz.block.bittycity.outie.models.WithdrawalToken import jakarta.inject.Singleton +import xyz.block.bittycity.outie.models.Failed import xyz.block.domainapi.util.Controller object DomainControllerModule : AbstractModule() { @@ -42,6 +43,7 @@ object DomainControllerModule : AbstractModule() { scamWarningController: ScamWarningController, travelRuleController: TravelRuleController, onChainController: OnChainController, + failedController: FailedController, ): DomainController { val stateToController: Map> = @@ -76,6 +78,7 @@ object DomainControllerModule : AbstractModule() { SubmittingOnChain to onChainController, WaitingForPendingConfirmationStatus to onChainController, WaitingForConfirmedOnChainStatus to onChainController, + Failed to failedController, ).mapValues { (_, controller) -> @Suppress("UNCHECKED_CAST") controller as Controller diff --git a/outie/src/main/kotlin/xyz/block/bittycity/outie/controllers/FailedController.kt b/outie/src/main/kotlin/xyz/block/bittycity/outie/controllers/FailedController.kt new file mode 100644 index 0000000..e50a050 --- /dev/null +++ b/outie/src/main/kotlin/xyz/block/bittycity/outie/controllers/FailedController.kt @@ -0,0 +1,39 @@ +package xyz.block.bittycity.outie.controllers + +import app.cash.kfsm.StateMachine +import arrow.core.raise.result +import jakarta.inject.Inject +import xyz.block.bittycity.outie.client.MetricsClient +import xyz.block.bittycity.outie.models.RequirementId +import xyz.block.bittycity.outie.models.Withdrawal +import xyz.block.bittycity.outie.models.WithdrawalState +import xyz.block.bittycity.outie.models.WithdrawalToken +import xyz.block.bittycity.outie.store.WithdrawalStore +import xyz.block.domainapi.DomainApiError +import xyz.block.domainapi.Input +import xyz.block.domainapi.ProcessingState +import xyz.block.domainapi.util.Operation + +class FailedController @Inject constructor( + stateMachine: StateMachine, + withdrawalStore: WithdrawalStore, + metricsClient: MetricsClient, +) : WithdrawalController(stateMachine, metricsClient, withdrawalStore) { + + override fun processInputs( + value: Withdrawal, + inputs: List>, + operation: Operation, + hurdleGroupId: String? + ): Result> = result { + raise(DomainApiError.InvalidProcessState(value.id.toString(), "Withdrawal is in failed state")) + } + + override fun handleFailure( + failure: Throwable, + value: Withdrawal + ): Result = result { + // Do nothing - withdrawal is already failed + raise(failure) + } +} \ No newline at end of file diff --git a/outie/src/test/kotlin/xyz/block/bittycity/outie/controllers/FailedControllerTest.kt b/outie/src/test/kotlin/xyz/block/bittycity/outie/controllers/FailedControllerTest.kt new file mode 100644 index 0000000..7588097 --- /dev/null +++ b/outie/src/test/kotlin/xyz/block/bittycity/outie/controllers/FailedControllerTest.kt @@ -0,0 +1,40 @@ +package xyz.block.bittycity.outie.controllers + +import io.kotest.matchers.result.shouldBeFailure +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.property.arbitrary.next +import jakarta.inject.Inject +import org.junit.jupiter.api.Test +import xyz.block.bittycity.outie.api.WithdrawalDomainController +import xyz.block.bittycity.outie.models.Failed +import xyz.block.bittycity.outie.models.FailureReason +import xyz.block.bittycity.outie.testing.Arbitrary +import xyz.block.bittycity.outie.testing.BittyCityTestCase +import xyz.block.domainapi.DomainApiError +import xyz.block.domainapi.util.Operation + +class FailedControllerTest : BittyCityTestCase() { + + @Inject lateinit var subject: WithdrawalDomainController + + @Test + fun `Operating on a failed withdrawal returns an InvalidProcessState error`() = runTest { + val withdrawal = data.seedWithdrawal( + state = Failed, + walletAddress = Arbitrary.walletAddress.next(), + amount = Arbitrary.bitcoins.next(), + ) { it.copy(failureReason = FailureReason.RISK_BLOCKED) } + + subject.execute( + withdrawal, + emptyList(), + Operation.EXECUTE + ).shouldBeFailure() + + withdrawalWithToken(withdrawal.id) should { + it.state shouldBe Failed + it.failureReason shouldBe FailureReason.RISK_BLOCKED + } + } +} \ No newline at end of file