Skip to content

Commit 4b68ba5

Browse files
feat: enable transfer rights enhancement (#186)
1 parent 103d6b6 commit 4b68ba5

File tree

13 files changed

+399
-228
lines changed

13 files changed

+399
-228
lines changed

src/app/assets/transfer-rights/transfer-rights.component.html

Lines changed: 160 additions & 143 deletions
Large diffs are not rendered by default.

src/app/assets/transfer-rights/transfer-rights.component.ts

Lines changed: 149 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/

src/assets/i18n/cn.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,24 @@
145145
"backButton": "返回资产"
146146
},
147147
"form": {
148+
"selectAsset": "选择资产",
148149
"sourceContract": "源合约",
149150
"destinationContract": "目标合约",
150-
"numberOfShares": "份额数量",
151+
"amount": "数量",
151152
"procedureFee": "程序费用",
152153
"tick": "刻度",
153154
"tickOverwrite": "手动刻度覆盖",
154155
"maxButton": "最大",
155156
"submit": "转移权利"
156157
},
157158
"errors": {
159+
"assetRequired": "需要选择资产",
158160
"sourceRequired": "需要源合约",
159161
"destinationRequired": "需要目标合约",
160162
"contractsEqual": "源合约和目标合约必须不同",
161-
"sharesRequired": "需要份额数量",
162-
"sharesMin": "份额数量必须至少为1",
163-
"sharesMax": "份额数量不能超过 {{max}}",
163+
"amount": "需要数量",
164+
"amountMin": "数量必须至少为1",
165+
"amountMax": "数量不能超过 {{max}}",
164166
"tickRequired": "需要刻度",
165167
"tickMin": "刻度必须至少为 {{min}}",
166168
"insufficientFees": "余额不足以支付交易费用",
@@ -170,11 +172,11 @@
170172
"publishFailed": "发布交易失败"
171173
},
172174
"messages": {
173-
"success": "成功提交刻度 {{tick}} 的转移权利交易"
175+
"success": "成功提交刻度 {{tick}} 的转移管理权交易"
174176
},
175177
"confirmDialog": {
176178
"title": "确认转移权利",
177-
"message": "您即将将 {{shares}} {{assetName}} {{sourceContract}} 转移到 {{destinationContract}}。程序费用为 {{fee}} QUBIC。您要继续吗?"
179+
"message": "您即将将 {{shares}} {{assetName}} 的管理权从 {{sourceContract}} 转移到 {{destinationContract}}。费用为 {{fee}} QUBIC。"
178180
}
179181
},
180182
"qrReceiveComponent": {

src/assets/i18n/de.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,24 @@
145145
"backButton": "Zurück zu Vermögenswerten"
146146
},
147147
"form": {
148+
"selectAsset": "Asset auswählen",
148149
"sourceContract": "Quellvertrag",
149150
"destinationContract": "Zielvertrag",
150-
"numberOfShares": "Anzahl der Anteile",
151+
"amount": "Menge",
151152
"procedureFee": "Verfahrensgebühr",
152153
"tick": "Tick",
153154
"tickOverwrite": "Manuelles Tick-Override",
154155
"maxButton": "Max",
155156
"submit": "Rechte übertragen"
156157
},
157158
"errors": {
159+
"assetRequired": "Asset-Auswahl ist erforderlich",
158160
"sourceRequired": "Quellvertrag ist erforderlich",
159161
"destinationRequired": "Zielvertrag ist erforderlich",
160162
"contractsEqual": "Quell- und Zielverträge müssen unterschiedlich sein",
161-
"sharesRequired": "Anzahl der Anteile ist erforderlich",
162-
"sharesMin": "Anzahl der Anteile muss mindestens 1 sein",
163-
"sharesMax": "Anzahl der Anteile darf {{max}} nicht überschreiten",
163+
"amount": "Menge ist erforderlich",
164+
"amountMin": "Menge muss mindestens 1 sein",
165+
"amountMax": "Menge darf {{max}} nicht überschreiten",
164166
"tickRequired": "Tick ist erforderlich",
165167
"tickMin": "Tick muss mindestens {{min}} sein",
166168
"insufficientFees": "Unzureichendes Guthaben zur Zahlung der Transaktionsgebühren",
@@ -170,11 +172,11 @@
170172
"publishFailed": "Veröffentlichung der Transaktion fehlgeschlagen"
171173
},
172174
"messages": {
173-
"success": "Übertragung der Verwaltungsrechte erfolgreich für Tick {{tick}} übermittelt"
175+
"success": "Transaktion zur Übertragung der Verwaltungsrechte erfolgreich für Tick {{tick}} übermittelt"
174176
},
175177
"confirmDialog": {
176178
"title": "Übertragung der Verwaltungsrechte bestätigen",
177-
"message": "Sie sind dabei, {{shares}} Anteile von {{assetName}} von {{sourceContract}} an {{destinationContract}} zu übertragen. Die Verfahrensgebühr beträgt {{fee}} QUBIC. Möchten Sie fortfahren?"
179+
"message": "Sie sind dabei, die Verwaltungsrechte für {{shares}} {{assetName}} von {{sourceContract}} an {{destinationContract}} zu übertragen. Die Gebühr beträgt {{fee}} QUBIC."
178180
}
179181
},
180182
"qrReceiveComponent": {

0 commit comments

Comments
 (0)