@@ -6,9 +6,14 @@ import com.getcode.opencode.controllers.TransactionController
66import com.getcode.opencode.model.accounts.AccountCluster
77import com.getcode.opencode.model.core.OpenCodePayload
88import com.getcode.opencode.model.core.PayloadKind
9+ import com.getcode.opencode.model.core.errors.SubmitIntentError
910import com.getcode.opencode.model.financial.Token
11+ import com.getcode.opencode.model.transactions.GiveRequest
1012import com.getcode.opencode.providers.TokenMetadataProvider
1113import com.getcode.solana.keys.Key32
14+ import com.getcode.solana.keys.Mint
15+ import io.mockk.coEvery
16+ import io.mockk.coVerify
1217import io.mockk.every
1318import io.mockk.mockk
1419import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -83,6 +88,79 @@ class GrabBillTransactorTest {
8388 assertTrue(result.isFailure || result.getOrNull()?.isFailure == true )
8489 }
8590
91+ @Test
92+ fun `MultiMintCash recovers from unexpected owner account via refreshAccountState then retry` () = runTest {
93+ val transactor = createTransactor(this )
94+ setupWithMultiMint(transactor)
95+
96+ val nonCoreMint = Mint (" nonCoreMint11111111111111111111111111111111" )
97+ val token = mockk<Token >(relaxed = true ) {
98+ every { address } returns nonCoreMint
99+ }
100+ val giveRequest = GiveRequest (
101+ messageId = Key32 .mock,
102+ mint = nonCoreMint,
103+ exchangeData = mockk(relaxed = true ),
104+ tokenMetadata = token
105+ )
106+
107+ coEvery { messagingController.pollForGiveRequest(any()) } returns Result .success(giveRequest)
108+ coEvery { accountController.hasAccountFor(nonCoreMint) } returns false
109+
110+ // First createUserAccount call fails with unexpected owner account
111+ coEvery {
112+ accountController.createUserAccount(any(), eq(nonCoreMint))
113+ } returns Result .failure(
114+ SubmitIntentError .Denied (listOf (" unexpected owner account" ))
115+ ) andThen Result .success(emptyList())
116+
117+ // The grab request will fail (not the focus of this test) but we verify recovery happened
118+ runCatching { transactor.start() }
119+
120+ coVerify(exactly = 1 ) {
121+ accountController.refreshAccountState()
122+ }
123+ coVerify(exactly = 2 ) {
124+ accountController.createUserAccount(any(), eq(nonCoreMint))
125+ }
126+ }
127+
128+ @Test
129+ fun `MultiMintCash does not recover from non-unexpected-owner denied error` () = runTest {
130+ val transactor = createTransactor(this )
131+ setupWithMultiMint(transactor)
132+
133+ val nonCoreMint = Mint (" nonCoreMint11111111111111111111111111111111" )
134+ val token = mockk<Token >(relaxed = true ) {
135+ every { address } returns nonCoreMint
136+ }
137+ val giveRequest = GiveRequest (
138+ messageId = Key32 .mock,
139+ mint = nonCoreMint,
140+ exchangeData = mockk(relaxed = true ),
141+ tokenMetadata = token
142+ )
143+
144+ coEvery { messagingController.pollForGiveRequest(any()) } returns Result .success(giveRequest)
145+ coEvery { accountController.hasAccountFor(nonCoreMint) } returns false
146+
147+ val deniedError = SubmitIntentError .Denied (listOf (" some other reason" ))
148+ coEvery {
149+ accountController.createUserAccount(any(), eq(nonCoreMint))
150+ } returns Result .failure(deniedError)
151+
152+ runCatching { transactor.start() }
153+
154+ // Should NOT trigger account bootstrap
155+ coVerify(exactly = 0 ) {
156+ accountController.refreshAccountState()
157+ }
158+ // Original call only happens once (no retry)
159+ coVerify(exactly = 1 ) {
160+ accountController.createUserAccount(any(), eq(nonCoreMint))
161+ }
162+ }
163+
86164 // endregion
87165
88166 // region dispose
@@ -106,6 +184,20 @@ class GrabBillTransactorTest {
106184
107185 // region helpers
108186
187+ private fun setupWithMultiMint (transactor : GrabBillTransactor ): Pair <AccountCluster , OpenCodePayload > {
188+ val owner = mockk<AccountCluster >(relaxed = true ) {
189+ every { withTimelockForToken(any<Token >()) } returns this
190+ every { vaultPublicKey } returns Key32 .mock
191+ every { authority } returns mockk(relaxed = true ) { every { keyPair } returns mockk(relaxed = true ) }
192+ }
193+ val payload = mockk<OpenCodePayload >(relaxed = true ) {
194+ every { kind } returns PayloadKind .MultiMintCash
195+ every { rendezvous } returns mockk(relaxed = true )
196+ }
197+ transactor.with (owner, payload)
198+ return owner to payload
199+ }
200+
109201 private fun setupWith (transactor : GrabBillTransactor , kind : PayloadKind ) {
110202 val owner = mockk<AccountCluster >(relaxed = true ) {
111203 every { withTimelockForToken(any<Token >()) } returns this
0 commit comments