Skip to content

Commit c76ffa4

Browse files
committed
feat: account activation status
1 parent f385cfa commit c76ffa4

6 files changed

Lines changed: 290 additions & 0 deletions

File tree

packages/snap/src/context.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ChangeTrustOptHandler,
99
ClientRequestHandler,
1010
ClientRequestMethod,
11+
GetStellarAccountActivationStatusHandler,
1112
} from './handlers/clientRequest';
1213
import type { ICronjobRequestHandler } from './handlers/cronjob/api';
1314
import { BackgroundEventMethod } from './handlers/cronjob/api';
@@ -206,11 +207,20 @@ const changeTrustOptHandler = new ChangeTrustOptHandler({
206207
confirmationUIController,
207208
});
208209

210+
const getStellarAccountActivationStatusHandler =
211+
new GetStellarAccountActivationStatusHandler({
212+
logger,
213+
accountService,
214+
networkService,
215+
});
216+
209217
const clientRequestMethodHandlers: Record<
210218
ClientRequestMethod,
211219
IClientRequestHandler
212220
> = {
213221
[ClientRequestMethod.ChangeTrustOpt]: changeTrustOptHandler,
222+
[ClientRequestMethod.GetStellarAccountActivationStatus]:
223+
getStellarAccountActivationStatusHandler,
214224
};
215225

216226
const clientRequestHandler = new ClientRequestHandler({

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { assert, StructError } from '@metamask/superstruct';
33
import {
44
ChangeTrustOptJsonRpcRequestStruct,
55
ChangeTrustOptJsonRpcResponseStruct,
6+
GetStellarAccountActivationStatusJsonRpcRequestStruct,
7+
GetStellarAccountActivationStatusJsonRpcResponseStruct,
68
JsonRpcRequestWithAccountStruct,
79
} from './api';
810

@@ -219,3 +221,84 @@ describe('ChangeTrustOptJsonRpcRequestStruct', () => {
219221
);
220222
});
221223
});
224+
225+
describe('GetStellarAccountActivationStatusJsonRpcRequestStruct', () => {
226+
it('accepts a valid getStellarAccountActivationStatus JSON-RPC request', () => {
227+
const request = {
228+
jsonrpc: '2.0' as const,
229+
id: 1,
230+
method: 'getStellarAccountActivationStatus',
231+
params: {
232+
accountId,
233+
scope,
234+
},
235+
};
236+
237+
expect(() =>
238+
assert(request, GetStellarAccountActivationStatusJsonRpcRequestStruct),
239+
).not.toThrow();
240+
});
241+
242+
it.each([
243+
{
244+
jsonrpc: '2.0' as const,
245+
id: 1,
246+
method: 'wrongMethod',
247+
params: {
248+
accountId,
249+
scope,
250+
},
251+
},
252+
{
253+
jsonrpc: '2.0' as const,
254+
id: 1,
255+
method: 'getStellarAccountActivationStatus',
256+
params: {
257+
accountId,
258+
scope: 'stellar:unknown',
259+
},
260+
},
261+
{
262+
jsonrpc: '2.0' as const,
263+
id: 1,
264+
method: 'getStellarAccountActivationStatus',
265+
params: {
266+
accountId: 'invalid-account-id',
267+
scope,
268+
},
269+
},
270+
])(
271+
'rejects an invalid getStellarAccountActivationStatus JSON-RPC request',
272+
(request) => {
273+
expect(() =>
274+
assert(request, GetStellarAccountActivationStatusJsonRpcRequestStruct),
275+
).toThrow(StructError);
276+
},
277+
);
278+
});
279+
280+
describe('GetStellarAccountActivationStatusJsonRpcResponseStruct', () => {
281+
it.each([{ activated: true }, { activated: false }])(
282+
'accepts a valid getStellarAccountActivationStatus JSON-RPC response',
283+
(response) => {
284+
expect(() =>
285+
assert(
286+
response,
287+
GetStellarAccountActivationStatusJsonRpcResponseStruct,
288+
),
289+
).not.toThrow();
290+
},
291+
);
292+
293+
it.each([{ activated: 'true' }, {}, { status: true }])(
294+
'rejects an invalid getStellarAccountActivationStatus JSON-RPC response',
295+
(response) => {
296+
expect(() =>
297+
assert(
298+
response,
299+
GetStellarAccountActivationStatusJsonRpcResponseStruct,
300+
),
301+
).toThrow(StructError);
302+
},
303+
);
304+
});

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
export enum ClientRequestMethod {
2929
/** -------------------------------- Stellar Specific -------------------------------- */
3030
ChangeTrustOpt = 'changeTrustOpt',
31+
GetStellarAccountActivationStatus = 'getStellarAccountActivationStatus',
3132
}
3233

3334
/**
@@ -109,6 +110,29 @@ export const ChangeTrustOptJsonRpcResponseStruct = object({
109110
transactionId: optional(base64(string())),
110111
});
111112

113+
const GetStellarAccountActivationStatusParamsStruct = object({
114+
accountId: UuidStruct,
115+
scope: KnownCaip2ChainIdStruct,
116+
});
117+
118+
/**
119+
* Validation struct for the getStellarAccountActivationStatus JSON-RPC request.
120+
*/
121+
export const GetStellarAccountActivationStatusJsonRpcRequestStruct = assign(
122+
JsonRpcRequestStruct,
123+
object({
124+
method: literal(ClientRequestMethod.GetStellarAccountActivationStatus),
125+
params: GetStellarAccountActivationStatusParamsStruct,
126+
}),
127+
);
128+
129+
/**
130+
* Validation struct for the getStellarAccountActivationStatus JSON-RPC response.
131+
*/
132+
export const GetStellarAccountActivationStatusJsonRpcResponseStruct = object({
133+
activated: boolean(),
134+
});
135+
112136
/**
113137
* A JSON-RPC request with an account resolve parameter.
114138
*/
@@ -130,3 +154,17 @@ export type ChangeTrustOptJsonRpcRequest = Infer<
130154
export type ChangeTrustOptJsonRpcResponse = Infer<
131155
typeof ChangeTrustOptJsonRpcResponseStruct
132156
>;
157+
158+
/**
159+
* Type for the getStellarAccountActivationStatus JSON-RPC request.
160+
*/
161+
export type GetStellarAccountActivationStatusJsonRpcRequest = Infer<
162+
typeof GetStellarAccountActivationStatusJsonRpcRequestStruct
163+
>;
164+
165+
/**
166+
* Type for the getStellarAccountActivationStatus JSON-RPC response.
167+
*/
168+
export type GetStellarAccountActivationStatusJsonRpcResponse = Infer<
169+
typeof GetStellarAccountActivationStatusJsonRpcResponseStruct
170+
>;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
ClientRequestMethod,
3+
type GetStellarAccountActivationStatusJsonRpcRequest,
4+
} from './api';
5+
import { GetStellarAccountActivationStatusHandler } from './getStellarAccountActivationStatus';
6+
import { KnownCaip2ChainId } from '../../api';
7+
import { AccountService } from '../../services/account';
8+
import { generateStellarKeyringAccount } from '../../services/account/__mocks__/account.fixtures';
9+
import { NetworkService } from '../../services/network';
10+
import { mockOnChainAccountService } from '../../services/on-chain-account/__mocks__/onChainAccount.fixtures';
11+
import { getTestWallet } from '../../services/wallet/__mocks__/wallet.fixtures';
12+
import { logger } from '../../utils/logger';
13+
14+
jest.mock('../../utils/logger');
15+
16+
describe('GetStellarAccountActivationStatusHandler', () => {
17+
const accountId = '11111111-1111-4111-8111-111111111111';
18+
const scope = KnownCaip2ChainId.Mainnet;
19+
20+
function buildRequest(): GetStellarAccountActivationStatusJsonRpcRequest {
21+
return {
22+
jsonrpc: '2.0',
23+
id: 1,
24+
method: ClientRequestMethod.GetStellarAccountActivationStatus,
25+
params: { accountId, scope },
26+
};
27+
}
28+
29+
it('returns activated true when Horizon has the account', async () => {
30+
const wallet = getTestWallet();
31+
const account = generateStellarKeyringAccount(
32+
accountId,
33+
wallet.address,
34+
'entropy-source-1',
35+
0,
36+
);
37+
38+
const resolveAccountSpy = jest
39+
.spyOn(AccountService.prototype, 'resolveAccount')
40+
.mockResolvedValue({ account });
41+
const getAccountOrNullSpy = jest
42+
.spyOn(NetworkService.prototype, 'getAccountOrNull')
43+
.mockResolvedValue({} as never);
44+
45+
const { accountService } = mockOnChainAccountService();
46+
const networkService = new NetworkService({ logger });
47+
const handler = new GetStellarAccountActivationStatusHandler({
48+
logger,
49+
accountService,
50+
networkService,
51+
});
52+
53+
expect(await handler.handle(buildRequest())).toStrictEqual({
54+
activated: true,
55+
});
56+
57+
expect(resolveAccountSpy).toHaveBeenCalledWith({ accountId });
58+
expect(getAccountOrNullSpy).toHaveBeenCalledWith(account.address, scope);
59+
});
60+
61+
it('returns activated false when Horizon has no account', async () => {
62+
const wallet = getTestWallet();
63+
const account = generateStellarKeyringAccount(
64+
accountId,
65+
wallet.address,
66+
'entropy-source-1',
67+
0,
68+
);
69+
70+
jest
71+
.spyOn(AccountService.prototype, 'resolveAccount')
72+
.mockResolvedValue({ account });
73+
const getAccountOrNullSpy = jest
74+
.spyOn(NetworkService.prototype, 'getAccountOrNull')
75+
.mockResolvedValue(null);
76+
77+
const { accountService } = mockOnChainAccountService();
78+
const networkService = new NetworkService({ logger });
79+
const handler = new GetStellarAccountActivationStatusHandler({
80+
logger,
81+
accountService,
82+
networkService,
83+
});
84+
85+
expect(await handler.handle(buildRequest())).toStrictEqual({
86+
activated: false,
87+
});
88+
89+
expect(getAccountOrNullSpy).toHaveBeenCalledWith(account.address, scope);
90+
});
91+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type {
2+
GetStellarAccountActivationStatusJsonRpcRequest,
3+
GetStellarAccountActivationStatusJsonRpcResponse,
4+
} from './api';
5+
import {
6+
GetStellarAccountActivationStatusJsonRpcRequestStruct,
7+
GetStellarAccountActivationStatusJsonRpcResponseStruct,
8+
} from './api';
9+
import type { AccountService } from '../../services/account';
10+
import type { NetworkService } from '../../services/network';
11+
import type { ILogger } from '../../utils/logger';
12+
import { createPrefixedLogger } from '../../utils/logger';
13+
import { BaseHandler } from '../base';
14+
15+
export class GetStellarAccountActivationStatusHandler extends BaseHandler<
16+
GetStellarAccountActivationStatusJsonRpcRequest,
17+
GetStellarAccountActivationStatusJsonRpcResponse
18+
> {
19+
readonly #accountService: AccountService;
20+
21+
readonly #networkService: NetworkService;
22+
23+
constructor({
24+
logger,
25+
accountService,
26+
networkService,
27+
}: {
28+
logger: ILogger;
29+
accountService: AccountService;
30+
networkService: NetworkService;
31+
}) {
32+
const prefixedLogger = createPrefixedLogger(
33+
logger,
34+
'[GetStellarAccountActivationStatusHandler]',
35+
);
36+
super({
37+
logger: prefixedLogger,
38+
requestStruct: GetStellarAccountActivationStatusJsonRpcRequestStruct,
39+
responseStruct: GetStellarAccountActivationStatusJsonRpcResponseStruct,
40+
});
41+
this.#accountService = accountService;
42+
this.#networkService = networkService;
43+
}
44+
45+
/**
46+
* Returns whether the account exists on Horizon for the requested network scope.
47+
*
48+
* @param request - JSON-RPC request with `accountId` and `scope`.
49+
* @returns `{ activated: true }` when Horizon has the account; `false` when missing / not funded on-ledger.
50+
*/
51+
protected async handleRequest(
52+
request: GetStellarAccountActivationStatusJsonRpcRequest,
53+
): Promise<GetStellarAccountActivationStatusJsonRpcResponse> {
54+
const { accountId, scope } = request.params;
55+
56+
const { account } = await this.#accountService.resolveAccount({
57+
accountId,
58+
});
59+
60+
const onChain = await this.#networkService.getAccountOrNull(
61+
account.address,
62+
scope,
63+
);
64+
65+
return { activated: onChain !== null };
66+
}
67+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './changeTrustOpt';
2+
export * from './getStellarAccountActivationStatus';
23
export * from './clientRequest';
34
export * from './api';
45
export type { IClientRequestHandler } from './base';

0 commit comments

Comments
 (0)