@@ -46,6 +46,15 @@ interface SourceContractOption extends ManagingContractOption {
4646 asset : QubicAsset ;
4747}
4848
49+ /**
50+ * Interface for asset selection dropdown
51+ */
52+ interface AssetOption {
53+ asset : QubicAsset ;
54+ totalAvailableBalance : number ;
55+ owningContracts : SourceContractOption [ ] ;
56+ }
57+
4958@Component ( {
5059 selector : 'app-transfer-rights' ,
5160 templateUrl : './transfer-rights.component.html' ,
@@ -70,10 +79,14 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
7079 private isLoadingAssets : boolean = false ;
7180
7281 // Asset and contract options
82+ public assets : AssetOption [ ] = [ ] ;
7383 public sourceContracts : SourceContractOption [ ] = [ ] ;
84+ public filteredSourceContracts : SourceContractOption [ ] = [ ] ;
7485 public destinationContracts : ManagingContractOption [ ] = [ ] ;
86+ public filteredDestinationContracts : ManagingContractOption [ ] = [ ] ;
7587
7688 // Selected values for dynamic updates
89+ public selectedAsset : AssetOption | null = null ;
7790 public selectedSourceContract : SourceContractOption | null = null ;
7891 public selectedDestinationContract : ManagingContractOption | null = null ;
7992
@@ -96,6 +109,7 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
96109 ) {
97110 // Initialize form
98111 this . transferRightsForm = this . fb . group ( {
112+ selectedAsset : [ '' , Validators . required ] ,
99113 sourceContract : [ '' , Validators . required ] ,
100114 destinationContract : [ '' , Validators . required ] ,
101115 numberOfShares : [ '' , [ Validators . required , Validators . min ( 1 ) ] ] ,
@@ -155,6 +169,12 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
155169 } ) ;
156170
157171 // Subscribe to form changes for dynamic updates
172+ this . transferRightsForm . get ( 'selectedAsset' ) ?. valueChanges
173+ . pipe ( takeUntil ( this . destroy$ ) )
174+ . subscribe ( value => {
175+ this . onAssetChange ( value ) ;
176+ } ) ;
177+
158178 this . transferRightsForm . get ( 'sourceContract' ) ?. valueChanges
159179 . pipe ( takeUntil ( this . destroy$ ) )
160180 . subscribe ( value => {
@@ -246,7 +266,9 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
246266 */
247267 private buildSourceContractOptions ( assets : QubicAsset [ ] ) : void {
248268 const contractMap = new Map < string , SourceContractOption > ( ) ;
269+ const assetMap = new Map < string , AssetOption > ( ) ;
249270
271+ // First pass: Build source contracts map (existing logic)
250272 for ( const asset of assets ) {
251273 if ( asset . ownedAmount <= 0 ) {
252274 continue ;
@@ -270,40 +292,74 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
270292 }
271293
272294 // Create unique key for this asset+contract combination
273- const key = `${ asset . publicId } -${ asset . assetName } -${ asset . issuerIdentity } -${ asset . contractIndex } ` ;
295+ const contractKey = `${ asset . publicId } -${ asset . assetName } -${ asset . issuerIdentity } -${ asset . contractIndex } ` ;
274296
275- if ( ! contractMap . has ( key ) ) {
276- contractMap . set ( key , {
297+ if ( ! contractMap . has ( contractKey ) ) {
298+ const sourceContract : SourceContractOption = {
277299 contractIndex : asset . contractIndex ,
278300 contractName : contract . label || contract . name ,
279301 address : contract . address ,
280302 procedureId : procedure . id ,
281303 procedureFee : procedure . fee ,
282304 availableBalance : asset . ownedAmount ,
283305 asset : asset
284- } ) ;
306+ } ;
307+ contractMap . set ( contractKey , sourceContract ) ;
308+
309+ // Group by asset (without contractIndex)
310+ const assetKey = `${ asset . publicId } -${ asset . assetName } -${ asset . issuerIdentity } ` ;
311+
312+ if ( ! assetMap . has ( assetKey ) ) {
313+ assetMap . set ( assetKey , {
314+ asset : asset ,
315+ totalAvailableBalance : 0 ,
316+ owningContracts : [ ]
317+ } ) ;
318+ }
319+
320+ const assetOption = assetMap . get ( assetKey ) ! ;
321+ assetOption . owningContracts . push ( sourceContract ) ;
322+ assetOption . totalAvailableBalance += asset . ownedAmount ;
285323 }
286324 }
287325
288- // Convert to array and sort alphabetically
326+ // Convert to arrays and sort alphabetically
289327 this . sourceContracts = Array . from ( contractMap . values ( ) )
290328 . sort ( ( a , b ) => a . contractName . localeCompare ( b . contractName , undefined , { sensitivity : 'base' } ) ) ;
291329
292- // Pre-select contract if we have pre-selected asset info
293- if ( this . preSelectedAsset && this . sourceContracts . length > 0 ) {
294- const matchingContract = this . sourceContracts . find ( c =>
295- c . asset . publicId === this . preSelectedAsset ! . publicId &&
296- c . asset . assetName === this . preSelectedAsset ! . assetName &&
297- c . asset . issuerIdentity === this . preSelectedAsset ! . issuerIdentity &&
298- c . contractIndex === this . preSelectedAsset ! . contractIndex
330+ this . assets = Array . from ( assetMap . values ( ) )
331+ . sort ( ( a , b ) => {
332+ const nameCompare = a . asset . assetName . localeCompare ( b . asset . assetName , undefined , { sensitivity : 'base' } ) ;
333+ if ( nameCompare !== 0 ) return nameCompare ;
334+ return a . asset . publicId . localeCompare ( b . asset . publicId , undefined , { sensitivity : 'base' } ) ;
335+ } ) ;
336+
337+ // Pre-select asset and contract if we have pre-selected asset info
338+ if ( this . preSelectedAsset && this . assets . length > 0 ) {
339+ const matchingAsset = this . assets . find ( a =>
340+ a . asset . publicId === this . preSelectedAsset ! . publicId &&
341+ a . asset . assetName === this . preSelectedAsset ! . assetName &&
342+ a . asset . issuerIdentity === this . preSelectedAsset ! . issuerIdentity
299343 ) ;
300344
301- if ( matchingContract ) {
302- // Use setTimeout to ensure the form is ready (same pattern as Send Assets)
345+ if ( matchingAsset ) {
346+ // Use setTimeout to ensure the form is ready
303347 setTimeout ( ( ) => {
348+ // First select the asset
304349 this . transferRightsForm . patchValue ( {
305- sourceContract : matchingContract
350+ selectedAsset : matchingAsset
306351 } ) ;
352+
353+ // Then find and select the matching contract
354+ const matchingContract = matchingAsset . owningContracts . find ( c =>
355+ c . contractIndex === this . preSelectedAsset ! . contractIndex
356+ ) ;
357+
358+ if ( matchingContract ) {
359+ this . transferRightsForm . patchValue ( {
360+ sourceContract : matchingContract
361+ } ) ;
362+ }
307363 } ) ;
308364 }
309365 }
@@ -342,6 +398,9 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
342398 // Sort alphabetically
343399 this . destinationContracts = contracts
344400 . sort ( ( a , b ) => a . contractName . localeCompare ( b . contractName , undefined , { sensitivity : 'base' } ) ) ;
401+
402+ // Initialize filtered list (will be filtered when source contract is selected)
403+ this . filteredDestinationContracts = this . destinationContracts ;
345404 }
346405
347406 /**
@@ -361,27 +420,35 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
361420 this . selectedSourceContract = sourceContract ;
362421
363422 if ( sourceContract ) {
423+ // Filter destination contracts to exclude the source contract
424+ this . filteredDestinationContracts = this . destinationContracts . filter (
425+ dest => dest . contractIndex !== sourceContract . contractIndex
426+ ) ;
427+
364428 // Update shares validation based on available balance
365429 this . updateSharesValidation ( ) ;
366430
367431 // Auto-select QX as destination if not managed by QX
368432 if ( sourceContract . address !== QubicDefinitions . QX_ADDRESS && ! this . transferRightsForm . get ( 'destinationContract' ) ?. value ) {
369- const qxContract = this . destinationContracts . find ( c => c . address === QubicDefinitions . QX_ADDRESS ) ;
433+ const qxContract = this . filteredDestinationContracts . find ( c => c . address === QubicDefinitions . QX_ADDRESS ) ;
370434 if ( qxContract ) {
371435 this . transferRightsForm . patchValue ( {
372436 destinationContract : qxContract
373437 } ) ;
374438 }
375439 }
376440
377- // Clear destination if same as source
441+ // Clear destination if same as source (shouldn't happen with filtering, but keep as safety)
378442 const destContract = this . transferRightsForm . get ( 'destinationContract' ) ?. value ;
379443 if ( destContract && destContract . contractIndex === sourceContract . contractIndex ) {
380444 this . transferRightsForm . patchValue ( {
381445 destinationContract : ''
382446 } ) ;
383447 }
384448 } else {
449+ // Reset filtered destination contracts to show all
450+ this . filteredDestinationContracts = this . destinationContracts ;
451+
385452 this . transferRightsForm . get ( 'numberOfShares' ) ?. clearValidators ( ) ;
386453 this . transferRightsForm . get ( 'numberOfShares' ) ?. addValidators ( [ Validators . required , Validators . min ( 1 ) ] ) ;
387454 this . transferRightsForm . get ( 'numberOfShares' ) ?. updateValueAndValidity ( ) ;
@@ -398,6 +465,53 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
398465 this . transferRightsForm . updateValueAndValidity ( ) ;
399466 }
400467
468+ /**
469+ * Handle asset selection change
470+ */
471+ private onAssetChange ( asset : AssetOption | null ) : void {
472+ this . selectedAsset = asset ;
473+
474+ if ( asset ) {
475+ // Filter source contracts to only show contracts managing this asset
476+ this . filteredSourceContracts = asset . owningContracts ;
477+
478+ // Reset numberOfShares when asset changes
479+ this . transferRightsForm . patchValue ( {
480+ numberOfShares : null
481+ } ) ;
482+
483+ // Auto-select first contract if only one option
484+ if ( this . filteredSourceContracts . length === 1 ) {
485+ this . transferRightsForm . patchValue ( {
486+ sourceContract : this . filteredSourceContracts [ 0 ]
487+ } ) ;
488+ } else {
489+ // Clear source contract selection
490+ this . transferRightsForm . patchValue ( {
491+ sourceContract : ''
492+ } ) ;
493+ }
494+ } else {
495+ this . filteredSourceContracts = [ ] ;
496+ this . transferRightsForm . patchValue ( {
497+ sourceContract : '' ,
498+ numberOfShares : null
499+ } ) ;
500+ }
501+ }
502+
503+ /**
504+ * Compare two assets for mat-select equality
505+ */
506+ public compareAssets ( a1 : AssetOption | null , a2 : AssetOption | null ) : boolean {
507+ if ( ! a1 || ! a2 ) {
508+ return a1 === a2 ;
509+ }
510+ return a1 . asset . assetName === a2 . asset . assetName &&
511+ a1 . asset . issuerIdentity === a2 . asset . issuerIdentity &&
512+ a1 . asset . publicId === a2 . asset . publicId ;
513+ }
514+
401515 /**
402516 * Update shares field validation based on available balance
403517 */
@@ -450,6 +564,24 @@ export class TransferRightsComponent implements OnInit, OnDestroy {
450564 return seed . balance >= this . getTransactionFee ( ) ;
451565 }
452566
567+ /**
568+ * Get balance after deducting transaction fees
569+ * Follows Asset Transfer pattern for consistency
570+ */
571+ public getBalanceAfterFees ( ) : number {
572+ if ( ! this . selectedSourceContract ) {
573+ return 0 ;
574+ }
575+
576+ const seed = this . walletService . getSeed ( this . selectedSourceContract . asset . publicId ) ;
577+ if ( ! seed ) {
578+ return 0 ;
579+ }
580+
581+ const balanceAfterFees = BigInt ( seed . balance ) - BigInt ( this . getTransactionFee ( ) ) ;
582+ return Number ( balanceAfterFees ) ;
583+ }
584+
453585 /**
454586 * Get seed alias for display
455587 */
0 commit comments