Skip to content

Commit 09fee1c

Browse files
authored
Merge pull request #51 from MetaMask/feat/chg-trust-refine
feat: Add Horizon-based transaction tracking and keyring settlement for trustline operation status tracking
2 parents 58cef90 + 2bf4311 commit 09fee1c

16 files changed

Lines changed: 1020 additions & 24 deletions

packages/site/src/pages/index.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -446,10 +446,6 @@ const Index = () => {
446446
}
447447

448448
const trimmedLimit = trustlineLimit.trim();
449-
if (action === 'delete' && !trimmedLimit) {
450-
setTrustlineOutput('For remove trustline, enter limit 0.');
451-
return;
452-
}
453449

454450
const account = resolveSelectedAccount();
455451
if (!account) {
@@ -473,8 +469,6 @@ const Index = () => {
473469
};
474470
if (action === 'add' && trimmedLimit) {
475471
params.limit = trimmedLimit;
476-
} else if (action === 'delete') {
477-
params.limit = '0';
478472
}
479473

480474
try {

packages/snap/src/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ const refreshConfirmationPricesHandler = new RefreshConfirmationPricesHandler({
162162

163163
const trackTransactionHandler = new TrackTransactionHandler({
164164
logger,
165+
networkService,
166+
onChainAccountService,
167+
accountService,
168+
transactionService,
165169
});
166170

167171
const syncAccountsHandler = new SyncAccountsHandler({

packages/snap/src/handlers/clientRequest/changeTrustOpt.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { getTestWallet } from '../../services/wallet/__mocks__/wallet.fixtures';
3838
import { ConfirmationInterfaceKey } from '../../ui/confirmation/api';
3939
import { ConfirmationUXController } from '../../ui/confirmation/controller';
4040
import { logger } from '../../utils/logger';
41+
import { TrackTransactionHandler } from '../cronjob/trackTransaction';
4142

4243
jest.mock('../../utils/logger');
4344
jest.mock('@metamask/keyring-snap-sdk', () => ({
@@ -48,6 +49,9 @@ describe('ChangeTrustOptHandler', () => {
4849
beforeEach(() => {
4950
jest.mocked(emitSnapKeyringEvent).mockReset();
5051
jest.mocked(emitSnapKeyringEvent).mockResolvedValue(undefined);
52+
jest
53+
.spyOn(TrackTransactionHandler, 'scheduleBackgroundEvent')
54+
.mockResolvedValue(undefined);
5155
});
5256

5357
const accountId = '11111111-1111-4111-8111-111111111111';
@@ -247,6 +251,13 @@ describe('ChangeTrustOptHandler', () => {
247251
},
248252
},
249253
});
254+
expect(
255+
TrackTransactionHandler.scheduleBackgroundEvent,
256+
).toHaveBeenCalledWith({
257+
txId: 'dGVzdC10eC1pZA==',
258+
scope,
259+
accountIds: [account.id],
260+
});
250261
});
251262

252263
it('returns success early for opt-in when trustline already exists', async () => {
@@ -269,6 +280,9 @@ describe('ChangeTrustOptHandler', () => {
269280
expect(signTransactionSpy).not.toHaveBeenCalled();
270281
expect(sendTransaction).not.toHaveBeenCalled();
271282
expect(savePendingKeyringTransaction).not.toHaveBeenCalled();
283+
expect(
284+
TrackTransactionHandler.scheduleBackgroundEvent,
285+
).not.toHaveBeenCalled();
272286
});
273287

274288
it('throws TrustlineNotFoundException for opt-out when trustline does not exist', async () => {
@@ -336,6 +350,13 @@ describe('ChangeTrustOptHandler', () => {
336350
},
337351
},
338352
});
353+
expect(
354+
TrackTransactionHandler.scheduleBackgroundEvent,
355+
).toHaveBeenCalledWith({
356+
txId: 'dGVzdC10eC1pZA==',
357+
scope,
358+
accountIds: [account.id],
359+
});
339360
});
340361

341362
it('throws UserRejectedRequestError when confirmation is rejected', async () => {
@@ -357,6 +378,9 @@ describe('ChangeTrustOptHandler', () => {
357378
expect(sendTransaction).not.toHaveBeenCalled();
358379
expect(networkSendSpy).not.toHaveBeenCalled();
359380
expect(savePendingKeyringTransaction).not.toHaveBeenCalled();
381+
expect(
382+
TrackTransactionHandler.scheduleBackgroundEvent,
383+
).not.toHaveBeenCalled();
360384
});
361385

362386
it('continues successfully when saving pending transaction fails', async () => {
@@ -373,5 +397,6 @@ describe('ChangeTrustOptHandler', () => {
373397
transactionId: 'dGVzdC10eC1pZA==',
374398
});
375399
expect(sendTransaction).toHaveBeenCalledTimes(1);
400+
expect(TrackTransactionHandler.scheduleBackgroundEvent).toHaveBeenCalled();
376401
});
377402
});

packages/snap/src/handlers/clientRequest/changeTrustOpt.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type { WalletService } from '../../services/wallet';
4141
import { ConfirmationInterfaceKey } from '../../ui/confirmation/api';
4242
import type { ConfirmationUXController } from '../../ui/confirmation/controller';
4343
import { createPrefixedLogger, type ILogger } from '../../utils/logger';
44+
import { TrackTransactionHandler } from '../cronjob/trackTransaction';
4445

4546
export class ChangeTrustOptHandler extends WithClientRequestActiveAccountResolve<
4647
ChangeTrustOptJsonRpcRequest,
@@ -164,6 +165,12 @@ export class ChangeTrustOptHandler extends WithClientRequestActiveAccountResolve
164165
action,
165166
});
166167

168+
await TrackTransactionHandler.scheduleBackgroundEvent({
169+
txId: transactionId,
170+
scope,
171+
accountIds: [account.id],
172+
});
173+
167174
return {
168175
status: true,
169176
transactionId,

packages/snap/src/handlers/cronjob/api.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ describe('Cronjob API structs', () => {
4949
expect(value).toStrictEqual({ accountIds: [id] });
5050
});
5151

52+
it('accepts empty object when accountIds is omitted', () => {
53+
const value = {};
54+
assert(value, SyncAccountParamsStruct);
55+
expect(value).toStrictEqual({});
56+
});
57+
5258
it('rejects empty account id arrays', () => {
5359
expect(() => assert({ accountIds: [] }, SyncAccountParamsStruct)).toThrow(
5460
StructError,
@@ -92,6 +98,20 @@ describe('Cronjob API structs', () => {
9298
});
9399
});
94100

101+
it('accepts synchronize accounts requests with empty params object', () => {
102+
const value = {
103+
...jsonRpcBase,
104+
method: BackgroundEventMethod.SynchronizeAccounts,
105+
params: {},
106+
};
107+
assert(value, SyncAccountJsonRpcRequestStruct);
108+
expect(value).toStrictEqual({
109+
...jsonRpcBase,
110+
method: BackgroundEventMethod.SynchronizeAccounts,
111+
params: {},
112+
});
113+
});
114+
95115
it('rejects wrong method for synchronize accounts request', () => {
96116
expect(() =>
97117
assert(

packages/snap/src/handlers/cronjob/api.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import {
44
assign,
55
boolean,
66
enums,
7+
integer,
78
literal,
89
nonempty,
910
object,
1011
optional,
12+
size,
1113
string,
1214
type,
1315
union,
@@ -48,10 +50,15 @@ export const TrackTransactionParamsStruct = type({
4850
txId: nonempty(string()),
4951
scope: KnownCaip2ChainIdStruct,
5052
accountIds: nonempty(array(UuidStruct)),
53+
/** Reschedule counter; omitted on first schedule (treated as 0). */
54+
attempt: optional(size(integer(), 0, 30)),
5155
});
5256

5357
export const SyncAccountParamsStruct = object({
54-
accountIds: union([nonempty(array(UuidStruct)), literal('selected')]),
58+
/** Omitted or undefined means “selected accounts” (declarative cron may send `{}`). */
59+
accountIds: optional(
60+
union([nonempty(array(UuidStruct)), literal('selected')]),
61+
),
5562
});
5663

5764
export const RefreshConfirmationPricesJsonRpcRequestStruct = assign(

packages/snap/src/handlers/cronjob/syncAccounts.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,27 @@ describe('SyncAccountsHandler', () => {
110110
);
111111
});
112112

113+
it('treats empty params object like selected accounts', async () => {
114+
const { handler, accountService, onChainAccountService } = setupTest();
115+
const selectedAccounts = [firstAccount];
116+
accountService.getAllSelected.mockResolvedValue(selectedAccounts);
117+
118+
const request = {
119+
jsonrpc: '2.0',
120+
id: 1,
121+
method: BackgroundEventMethod.SynchronizeAccounts,
122+
params: {},
123+
};
124+
125+
await handler.handle(request);
126+
127+
expect(accountService.getAllSelected).toHaveBeenCalledTimes(1);
128+
expect(onChainAccountService.synchronize).toHaveBeenCalledWith(
129+
selectedAccounts,
130+
AppConfig.selectedNetwork,
131+
);
132+
});
133+
113134
it('synchronizes accounts fetched by ids when accountIds is an array of account ids', async () => {
114135
const { handler, accountService, onChainAccountService } = setupTest();
115136
const accountIds = [firstAccount.id, secondAccount.id];

packages/snap/src/handlers/cronjob/syncAccounts.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,13 @@ export class SyncAccountsHandler extends CronjobBaseHandler<SyncAccountJsonRpcRe
5656
* `JSON.parse(JSON.stringify(jobs))` validated successfully (same logical shape).
5757
* Omitted params mean "selected accounts" here.
5858
*
59-
* @param request SyncAccountJsonRpcRequest
59+
* @param request - Cron JSON-RPC request (params may be omitted or `{}`).
6060
*/
6161
protected async handleCronJobRequest(
6262
request: SyncAccountJsonRpcRequest,
6363
): Promise<void> {
6464
const scope = AppConfig.selectedNetwork;
65-
const accountIds =
66-
request.params === undefined
67-
? ('selected' as const)
68-
: request.params.accountIds;
65+
const accountIds = request.params?.accountIds ?? ('selected' as const);
6966

7067
let accounts: StellarKeyringAccount[] = [];
7168
if (accountIds === 'selected') {

0 commit comments

Comments
 (0)