@@ -24,6 +24,7 @@ public class CreditsPurchaseService : ICreditsPurchaseService
2424 private readonly CreditsManagerMetaTxRelayer metaTxRelayer ;
2525 private readonly PolygonSettlementPoller settlementPoller ;
2626 private readonly ManaUsdRateReader manaUsdRateReader ;
27+ private readonly CreditsChainConfig chainConfig ;
2728 private readonly IWeb3IdentityCache identityCache ;
2829 private readonly CreditsFeatureAccess creditsFeatureAccess ;
2930 private readonly bool isFeatureEnabled ;
@@ -36,6 +37,7 @@ public CreditsPurchaseService(
3637 CreditsManagerMetaTxRelayer metaTxRelayer ,
3738 PolygonSettlementPoller settlementPoller ,
3839 ManaUsdRateReader manaUsdRateReader ,
40+ CreditsChainConfig chainConfig ,
3941 IWeb3IdentityCache identityCache ,
4042 CreditsFeatureAccess creditsFeatureAccess ,
4143 bool isFeatureEnabled )
@@ -45,12 +47,13 @@ public CreditsPurchaseService(
4547 this . metaTxRelayer = metaTxRelayer ;
4648 this . settlementPoller = settlementPoller ;
4749 this . manaUsdRateReader = manaUsdRateReader ;
50+ this . chainConfig = chainConfig ;
4851 this . identityCache = identityCache ;
4952 this . creditsFeatureAccess = creditsFeatureAccess ;
5053 this . isFeatureEnabled = isFeatureEnabled ;
5154 }
5255
53- public async UniTask < CreditsQuoteResult > QuoteAsync ( string tradeId , CancellationToken ct )
56+ public async UniTask < CreditsQuoteResult > QuoteAsync ( ShopListingDto listing , CancellationToken ct )
5457 {
5558 if ( ! isFeatureEnabled || ! creditsFeatureAccess . IsUserAllowed ( ) )
5659 return new CreditsQuoteResult ( CreditsPurchaseError . FeatureDisabled ) ;
@@ -62,7 +65,12 @@ public async UniTask<CreditsQuoteResult> QuoteAsync(string tradeId, Cancellation
6265
6366 SetState ( CreditsPurchaseState . ResolvingListing ) ;
6467
65- try { return await QuoteInternalAsync ( tradeId , identity . Address , ct ) ; }
68+ try
69+ {
70+ return IsStoreMint ( listing )
71+ ? await QuoteStoreMintInternalAsync ( listing , ct )
72+ : await QuoteInternalAsync ( listing . tradeId , identity . Address , ct ) ;
73+ }
6674 catch ( OperationCanceledException )
6775 {
6876 return new CreditsQuoteResult ( CreditsPurchaseError . Cancelled ) ;
@@ -96,6 +104,42 @@ public async UniTask<CreditsPurchaseResult> PurchaseAsync(CreditsPurchaseQuote q
96104 }
97105 }
98106
107+ private static bool IsStoreMint ( ShopListingDto listing ) =>
108+ string . Equals ( listing . acquisition , "store" , StringComparison . OrdinalIgnoreCase ) ;
109+
110+ private async UniTask < CreditsQuoteResult > QuoteStoreMintInternalAsync ( ShopListingDto listing , CancellationToken ct )
111+ {
112+ if ( string . IsNullOrEmpty ( listing . itemId ) )
113+ return new CreditsQuoteResult ( CreditsPurchaseError . ListingNotAvailable , "Mint listing has no itemId" ) ;
114+
115+ if ( listing . available <= 0 )
116+ return new CreditsQuoteResult ( CreditsPurchaseError . ListingNotAvailable , "Mint is sold out" ) ;
117+
118+ if ( string . IsNullOrEmpty ( listing . manaWei ) )
119+ return new CreditsQuoteResult ( CreditsPurchaseError . ListingNotAvailable , "Mint listing has no price" ) ;
120+
121+ ManaUsdRate rate ;
122+
123+ try { rate = await manaUsdRateReader . ReadAsync ( chainConfig . OffChainMarketplaceAddress , ct ) ; }
124+ catch ( OperationCanceledException ) { throw ; }
125+ catch ( Exception e )
126+ {
127+ ReportHub . LogWarning ( ReportCategory . CREDITS_PURCHASE ,
128+ $ "MANA/USD rate unavailable for mint { listing . contractAddress } -{ listing . itemId } : { e . Message } ") ;
129+
130+ return new CreditsQuoteResult ( CreditsPurchaseError . PriceUnavailable , e . Message ) ;
131+ }
132+
133+ int usdCents = CreditsTradeEncoder . RoundUpToWholeCredit ( CreditsTradeEncoder . ManaWeiToUsdCents ( listing . manaWei ! , rate ) , CENTS_PER_CREDIT ) ;
134+ BigInteger requiredManaWei = CreditsTradeEncoder . AmountOrZero ( listing . manaWei ! ) ;
135+
136+ if ( usdCents <= 0 || requiredManaWei <= BigInteger . Zero )
137+ return new CreditsQuoteResult ( CreditsPurchaseError . ListingNotAvailable , "Mint has no price" ) ;
138+
139+ var target = new StoreMintTarget ( listing . contractAddress , listing . itemId ! , listing . manaWei ! ) ;
140+ return CreditsQuoteResult . Ok ( CreditsPurchaseQuote . ForMint ( target , usdCents , usdCents / CENTS_PER_CREDIT , requiredManaWei ) ) ;
141+ }
142+
99143 private async UniTask < CreditsQuoteResult > QuoteInternalAsync ( string tradeId , string buyer , CancellationToken ct )
100144 {
101145 TradeDto ? trade ;
@@ -161,28 +205,54 @@ private async UniTask<CreditsQuoteResult> QuoteInternalAsync(string tradeId, str
161205 if ( usdCents <= 0 )
162206 return new CreditsQuoteResult ( CreditsPurchaseError . ListingNotAvailable , "Trade has no price" ) ;
163207
164- return CreditsQuoteResult . Ok ( new CreditsPurchaseQuote ( trade , usdCents , usdCents / CENTS_PER_CREDIT , requiredManaWei , isLegacyMana ) ) ;
208+ return CreditsQuoteResult . Ok ( CreditsPurchaseQuote . ForTrade ( trade , usdCents , usdCents / CENTS_PER_CREDIT , requiredManaWei , isLegacyMana ) ) ;
165209 }
166210
167211 private async UniTask < CreditsPurchaseResult > PurchaseInternalAsync ( CreditsPurchaseQuote quote , string buyer , CancellationToken ct )
168212 {
169213 BigInteger requiredManaWei = quote . RequiredManaWei ;
214+ StoreMintTarget mint = quote . Mint ;
170215
171- if ( ! quote . IsLiveRatePrice )
216+ if ( quote . Kind == CreditsListingKind . StoreMint )
217+ {
218+ SetState ( CreditsPurchaseState . ResolvingListing ) ;
219+
220+ ShopListingDto ? fresh ;
221+
222+ try { fresh = await shopAPIClient . GetShopListingForItemAsync ( mint . CollectionAddress , mint . ItemId , ct ) ; }
223+ catch ( OperationCanceledException ) { throw ; }
224+ catch ( Exception e )
225+ {
226+ ReportHub . LogWarning ( ReportCategory . CREDITS_PURCHASE ,
227+ $ "Mint { mint . CollectionAddress } -{ mint . ItemId } could not be re-read before buying: { e . Message } ") ;
228+
229+ return Fail ( CreditsPurchaseError . ListingNotAvailable , message : e . Message ) ;
230+ }
231+
232+ if ( fresh == null || ! IsStoreMint ( fresh ) || fresh . available <= 0 || string . IsNullOrEmpty ( fresh . manaWei ) )
233+ return Fail ( CreditsPurchaseError . ListingNotAvailable , message : "The mint is no longer available" ) ;
234+
235+ mint = new StoreMintTarget ( mint . CollectionAddress , mint . ItemId , fresh . manaWei ! ) ;
236+ requiredManaWei = CreditsTradeEncoder . AmountOrZero ( fresh . manaWei ! ) ;
237+
238+ if ( requiredManaWei > quote . RequiredManaWei )
239+ return Fail ( CreditsPurchaseError . PriceChanged , message : "The mint price changed" ) ;
240+ }
241+ else if ( ! quote . IsLiveRatePrice )
172242 {
173243 SetState ( CreditsPurchaseState . ResolvingListing ) ;
174244
175245 ManaUsdRate rate ;
176246
177- try { rate = await manaUsdRateReader . ReadAsync ( quote . Trade . contract , ct ) ; }
247+ try { rate = await manaUsdRateReader . ReadAsync ( quote . Trade ! . contract , ct ) ; }
178248 catch ( OperationCanceledException ) { throw ; }
179249 catch ( Exception e )
180250 {
181- ReportHub . LogWarning ( ReportCategory . CREDITS_PURCHASE , $ "MANA/USD rate unavailable at purchase for trade { quote . Trade . id } : { e . Message } ") ;
251+ ReportHub . LogWarning ( ReportCategory . CREDITS_PURCHASE , $ "MANA/USD rate unavailable at purchase for trade { quote . Trade ! . id } : { e . Message } ") ;
182252 return Fail ( CreditsPurchaseError . PriceUnavailable , message : e . Message ) ;
183253 }
184254
185- requiredManaWei = CreditsTradeEncoder . UsdWeiToManaWei ( quote . Trade . received [ 0 ] . amount , rate ) ;
255+ requiredManaWei = CreditsTradeEncoder . UsdWeiToManaWei ( quote . Trade ! . received [ 0 ] . amount , rate ) ;
186256
187257 if ( requiredManaWei <= BigInteger . Zero )
188258 return Fail ( CreditsPurchaseError . PriceUnavailable , message : "Trade draws no MANA" ) ;
@@ -192,7 +262,7 @@ private async UniTask<CreditsPurchaseResult> PurchaseInternalAsync(CreditsPurcha
192262
193263 AuthorizeCreditResponse authorization ;
194264
195- try { authorization = await creditsAPIClient . AuthorizeUsdCreditAsync ( quote . UsdCents , quote . Trade . id , ct ) ; }
265+ try { authorization = await creditsAPIClient . AuthorizeUsdCreditAsync ( quote . UsdCents , quote . TradeId , quote . ContractAddress , quote . ItemId , ct ) ; }
196266 catch ( OperationCanceledException ) { throw ; }
197267 catch ( UnityWebRequestException e )
198268 {
@@ -221,10 +291,16 @@ private async UniTask<CreditsPurchaseResult> PurchaseInternalAsync(CreditsPurcha
221291 authorizedCap = BigInteger . Parse ( authorization . maxCreditedValue )
222292 + CreditsTradeEncoder . UncreditedValue ( authorization . maxCreditedValue , authorization . credit . availableAmount ) ;
223293
224- useCreditsCalldata = CreditsTradeEncoder . BuildUseCreditsCalldata (
225- quote . Trade , buyer , authorization . credit , authorization . maxCreditedValue ,
226- DateTimeOffset . UtcNow . ToUnixTimeSeconds ( ) + EXTERNAL_CALL_TTL_SECONDS ,
227- RandomSalt ( ) ) ;
294+ long externalCallExpiresAt = DateTimeOffset . UtcNow . ToUnixTimeSeconds ( ) + EXTERNAL_CALL_TTL_SECONDS ;
295+
296+ useCreditsCalldata = quote . Kind == CreditsListingKind . StoreMint
297+ ? CreditsTradeEncoder . BuildStoreMintUseCreditsCalldata (
298+ chainConfig . CollectionStoreAddress , mint . CollectionAddress , mint . ItemId , mint . PriceWei ,
299+ buyer , authorization . credit , authorization . maxCreditedValue ,
300+ externalCallExpiresAt , RandomSalt ( ) )
301+ : CreditsTradeEncoder . BuildUseCreditsCalldata (
302+ quote . Trade ! , buyer , authorization . credit , authorization . maxCreditedValue ,
303+ externalCallExpiresAt , RandomSalt ( ) ) ;
228304 }
229305 catch ( Exception e )
230306 {
@@ -236,10 +312,10 @@ private async UniTask<CreditsPurchaseResult> PurchaseInternalAsync(CreditsPurcha
236312 if ( authorizedCap < requiredManaWei )
237313 {
238314 ReportHub . LogWarning ( ReportCategory . CREDITS_PURCHASE ,
239- $ "Authorized cap { authorizedCap } wei cannot cover the { requiredManaWei } wei trade { quote . Trade . id } draws") ;
315+ $ "Authorized cap { authorizedCap } wei cannot cover the { requiredManaWei } wei { QuoteLabel ( quote ) } draws") ;
240316
241317 await ReleaseIntentAsync ( authorization . credit . id ) ;
242- return Fail ( CreditsPurchaseError . PriceChanged , message : "The authorized credit cannot cover this trade " ) ;
318+ return Fail ( CreditsPurchaseError . PriceChanged , message : "The authorized credit cannot cover this purchase " ) ;
243319 }
244320
245321 SetState ( CreditsPurchaseState . Signing ) ;
@@ -269,7 +345,7 @@ private async UniTask<CreditsPurchaseResult> PurchaseInternalAsync(CreditsPurcha
269345 SetState ( CreditsPurchaseState . Failed ) ;
270346 return new CreditsPurchaseResult ( CreditsPurchaseError . SettlementPending , message : relay . Message ) ;
271347 case RelayOutcome . RelayerRejected :
272- ReportHub . LogWarning ( ReportCategory . CREDITS_PURCHASE , $ "Relayer refused trade { quote . Trade . id } : { relay . Message } ") ;
348+ ReportHub . LogWarning ( ReportCategory . CREDITS_PURCHASE , $ "Relayer refused { QuoteLabel ( quote ) } : { relay . Message } ") ;
273349 await ReleaseIntentAsync ( authorization . credit . id ) ;
274350 return Fail ( CreditsPurchaseError . RelayerUnavailable , message : relay . Message ) ;
275351 }
@@ -298,6 +374,11 @@ private async UniTask<CreditsPurchaseResult> PurchaseInternalAsync(CreditsPurcha
298374 }
299375 }
300376
377+ private static string QuoteLabel ( in CreditsPurchaseQuote quote ) =>
378+ quote . Kind == CreditsListingKind . StoreMint
379+ ? $ "mint { quote . Mint . CollectionAddress } -{ quote . Mint . ItemId } "
380+ : $ "trade { quote . Trade ! . id } ";
381+
301382 private async UniTask ReleaseIntentAsync ( string creditId )
302383 {
303384 using var timeoutCts = new CancellationTokenSource ( RELEASE_INTENT_TIMEOUT ) ;
0 commit comments