Skip to content

Commit 8fcd351

Browse files
authored
feat(web-client): modify XRP trustline user flow (#328)
* refactor(web-client): add optional 'flags' parameter to createUnsignedTrustSetTx * fix(web-client): pause opportunistic XRPL token opt-in * style(web-client): fix typo * feat(web-client): add createTrustLine * feat(web-client): default all XRPL Trustline opt-ins when deleting XRPL account * fix(web-client): use absolute path instead of relative path
1 parent 7e7e124 commit 8fcd351

File tree

4 files changed

+173
-12
lines changed

4 files changed

+173
-12
lines changed

web-client/src/app/services/xrpl.service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,18 @@ export class XrplService {
149149

150150
async createUnsignedTrustSetTx(
151151
fromAddress: string,
152-
limitAmount: IssuedCurrencyAmount
152+
limitAmount: IssuedCurrencyAmount,
153+
flags?: number | xrpl.TrustSetFlagsInterface
153154
): Promise<xrpl.TrustSet> {
154155
const unpreparedTx: xrpl.TrustSet = {
155156
Account: fromAddress,
156157
TransactionType: 'TrustSet',
157158
LimitAmount: limitAmount,
158159
};
160+
if (typeof flags !== 'undefined') {
161+
unpreparedTx.Flags = flags;
162+
}
163+
159164
return await this.withConnection(
160165
async (client) => await client.autofill(unpreparedTx)
161166
);

web-client/src/app/state/session-xrpl.service.ts

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import { firstValueFrom } from 'rxjs';
3-
import { EnclaveService } from 'src/app/services/enclave/index';
3+
import { EnclaveService } from 'src/app/services/enclave';
44
import { XrplService } from 'src/app/services/xrpl.service';
55
import {
66
checkTxResponseSucceeded,
@@ -16,7 +16,7 @@ import { parseNumber } from 'src/app/utils/validators';
1616
import { ifDefined } from 'src/helpers/helpers';
1717
import { TransactionSigned, TransactionToSign } from 'src/schema/actions';
1818
import * as xrpl from 'xrpl';
19-
import { IssuedCurrencyAmount } from 'xrpl/dist/npm/models/common/index';
19+
import { IssuedCurrencyAmount } from 'xrpl/dist/npm/models/common';
2020
import { Trustline } from 'xrpl/dist/npm/models/methods/accountLines';
2121
import { ConnectorQuery } from './connector';
2222
import { SessionQuery } from './session.query';
@@ -206,7 +206,8 @@ export class SessionXrplService {
206206
* @see XrplService.createUnsignedTrustSetTx
207207
*/
208208
async sendTrustSetTx(
209-
limitAmount: IssuedCurrencyAmount
209+
limitAmount: IssuedCurrencyAmount,
210+
flags?: number | xrpl.TrustSetFlagsInterface
210211
): Promise<xrpl.TxResponse> {
211212
const { wallet } = this.sessionQuery.assumeActiveSession();
212213

@@ -215,7 +216,8 @@ export class SessionXrplService {
215216
async () =>
216217
await this.xrplService.createUnsignedTrustSetTx(
217218
wallet.xrpl_account.address_base58,
218-
limitAmount
219+
limitAmount,
220+
flags
219221
),
220222
{ from: wallet.xrpl_account.address_base58, limitAmount }
221223
);
@@ -253,11 +255,11 @@ export class SessionXrplService {
253255
(await firstValueFrom(this.sessionQuery.xrplTrustlines)) ?? [];
254256

255257
const txResponses: xrpl.TxResponse[] = [];
256-
for (const trustLine of trustLines) {
257-
ifDefined(await this.checkTrustlineOptIn(trustLine), (txResponse) =>
258-
txResponses.push(txResponse)
259-
);
260-
}
258+
// for (const trustLine of trustLines) {
259+
// ifDefined(await this.checkTrustlineOptIn(trustLine), (txResponse) =>
260+
// txResponses.push(txResponse)
261+
// );
262+
// }
261263
return txResponses;
262264
}
263265

@@ -318,6 +320,115 @@ export class SessionXrplService {
318320
}
319321
}
320322

323+
/**
324+
* Helper: Create a trust line between active session's wallet and targeted account.
325+
*
326+
* This creates and sends a `TrustSet` transaction.
327+
*
328+
* @return the `TrustSet` response, or undefined
329+
*/
330+
async createTrustline(
331+
currency: string,
332+
issuer: string,
333+
value: string,
334+
rippling: boolean
335+
): Promise<xrpl.TxResponse | undefined> {
336+
const limitAmount = {
337+
currency,
338+
issuer,
339+
value,
340+
};
341+
342+
if (rippling) {
343+
const enableRippling: xrpl.TrustSetFlagsInterface = {
344+
tfClearNoRipple: true,
345+
};
346+
return await withLoggedExchange(
347+
'SessionXrplService.createTrustline: sending TrustSet',
348+
async () => await this.sendTrustSetTx(limitAmount, enableRippling),
349+
limitAmount
350+
);
351+
}
352+
353+
return await withLoggedExchange(
354+
'SessionXrplService.createTrustline: sending TrustSet',
355+
async () => await this.sendTrustSetTx(limitAmount),
356+
limitAmount
357+
);
358+
}
359+
360+
/**
361+
* Default trustline opt-in for each of this account's trust lines.
362+
*
363+
* @return The responses to `TrustSet` transactions sent out (empty if none sent)
364+
* @see defaultTrustlineOptIn
365+
*/
366+
async defaultTrustlineOptIns(): Promise<xrpl.TxResponse[]> {
367+
// TODO(Pi): Check for necessary owner reserves before sending.
368+
// See: https://xrpl.org/reserves.html
369+
370+
const trustLines =
371+
(await firstValueFrom(this.sessionQuery.xrplTrustlines)) ?? [];
372+
373+
const txResponses: xrpl.TxResponse[] = [];
374+
for (const trustLine of trustLines) {
375+
ifDefined(await this.defaultTrustlineOptIn(trustLine), (txResponse) =>
376+
txResponses.push(txResponse)
377+
);
378+
}
379+
return txResponses;
380+
}
381+
382+
/**
383+
* Helper: Default trustline opt-in for the given trust-line.
384+
*
385+
* This sends a `TrustSet` transaction defaulting the limit to zero.
386+
*
387+
* @return the `TrustSet` response, or undefined
388+
*/
389+
async defaultTrustlineOptIn(
390+
trustline: Trustline
391+
): Promise<xrpl.TxResponse | undefined> {
392+
const limit_peer = parseNumber(trustline.limit_peer);
393+
if (limit_peer === undefined) {
394+
throw panic(
395+
'SessionXrplService.defaultTrustlineOptIn: bad limit_peer:',
396+
trustline
397+
);
398+
}
399+
if (limit_peer !== 0) {
400+
throw panic(
401+
'SessionXrplService.defaultTrustlineOptIn: limit_peer is not zero:',
402+
trustline
403+
);
404+
}
405+
406+
const limit = parseNumber(trustline.limit);
407+
if (limit === undefined) {
408+
throw panic(
409+
'SessionXrplService.defaultTrustlineOptIn: bad limit:',
410+
trustline
411+
);
412+
}
413+
414+
if (0 < limit) {
415+
const limitAmount = {
416+
currency: trustline.currency,
417+
issuer: trustline.account,
418+
value: '0',
419+
};
420+
const defaultFlags: xrpl.TrustSetFlagsInterface = {
421+
tfSetNoRipple: true,
422+
tfClearFreeze: true,
423+
};
424+
return await withLoggedExchange(
425+
'SessionXrplService.defaultTrustlineOptIn: sending TrustSet',
426+
async () => await this.sendTrustSetTx(limitAmount, defaultFlags),
427+
limitAmount
428+
);
429+
}
430+
}
431+
321432
protected async prepareUnsignedTransaction(
322433
receiverId: string,
323434
amount: xrpl.Payment['Amount']

web-client/src/app/views/delete-user/delete-user.page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export class DeleteUserPage implements OnInit {
138138
async refreshWalletData(): Promise<void> {
139139
this.balancesIsLoading = true;
140140
try {
141-
await withConsoleGroup('WalletPage.refreshWalletData:', async () => {
141+
await withConsoleGroup('DeleteUserPage.refreshWalletData:', async () => {
142142
await withConsoleGroupCollapsed('Loading wallet data', async () => {
143143
await Promise.all([
144144
(async () => {
@@ -207,7 +207,7 @@ export class DeleteUserPage implements OnInit {
207207
});
208208
if (0 < unsuccessfulResponses.length) {
209209
console.log(
210-
'DekewteUserPage.checkXrplTokenOptIns: unsuccessful responses:',
210+
'DeleteUserPage.checkXrplTokenOptIns: unsuccessful responses:',
211211
{ unsuccessfulResponses }
212212
);
213213
const errorMessage: string = unsuccessfulResponses

web-client/src/app/views/deposit-funds/deposit-funds.page.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { LoadingController, NavController } from '@ionic/angular';
44
import { checkTxResponseSucceeded } from 'src/app/services/xrpl.utils';
55
import { SessionXrplService } from 'src/app/state/session-xrpl.service';
66
import { SessionQuery } from 'src/app/state/session.query';
7+
import { withConsoleGroupCollapsed } from 'src/app/utils/console.helpers';
78
import { withLoadingOverlayOpts } from 'src/app/utils/loading.helpers';
89
import { SwalHelper } from 'src/app/utils/notification/swal-helper';
910
import * as xrpl from 'xrpl';
@@ -39,6 +40,13 @@ export class DepositFundsPage implements OnInit {
3940
}
4041

4142
async deleteWallet(): Promise<void> {
43+
await withConsoleGroupCollapsed(
44+
'Defaulting asset / token opt-ins',
45+
async () => {
46+
await this.defaultXrplTokenOptIns();
47+
}
48+
);
49+
4250
if (this.receiverAddress) {
4351
const result = await withLoadingOverlayOpts<
4452
{ xrplResult: TxResponse } | undefined
@@ -54,6 +62,43 @@ export class DepositFundsPage implements OnInit {
5462
}
5563
}
5664

65+
protected async defaultXrplTokenOptIns(): Promise<void> {
66+
if (this.sessionQuery.hasXrpBalance()) {
67+
const txResponses =
68+
await this.sessionXrplService.defaultTrustlineOptIns();
69+
const unsuccessfulResponses = txResponses.filter((txResponse) => {
70+
const { succeeded } = checkTxResponseSucceeded(txResponse);
71+
return !succeeded;
72+
});
73+
if (0 < unsuccessfulResponses.length) {
74+
console.log(
75+
'DepositFundsPage.defaultXrplTokenOptIns: unsuccessful responses:',
76+
{ unsuccessfulResponses }
77+
);
78+
const errorMessage: string = unsuccessfulResponses
79+
.map((txResponse) => {
80+
const { resultCode } = checkTxResponseSucceeded(txResponse);
81+
return resultCode;
82+
})
83+
.join('\n');
84+
await this.errorNotification('XRPL token opt-out failed', errorMessage);
85+
}
86+
}
87+
}
88+
89+
protected async errorNotification(
90+
titleText: string,
91+
err: any
92+
): Promise<void> {
93+
const text = err?.response?.body?.message ?? err?.response?.body ?? err;
94+
console.error('DepositFundsPage.withAlertErrors caught', { err });
95+
await this.notification.swal.fire({
96+
icon: 'error',
97+
titleText,
98+
text,
99+
});
100+
}
101+
57102
protected async deleteByLedgerType(
58103
receiverAddress: string
59104
): Promise<{ xrplResult: TxResponse }> {

0 commit comments

Comments
 (0)