Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/snap/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ChangeTrustOptHandler,
ClientRequestHandler,
ClientRequestMethod,
GetStellarAccountActivationStatusHandler,
} from './handlers/clientRequest';
import type { ICronjobRequestHandler } from './handlers/cronjob/api';
import { BackgroundEventMethod } from './handlers/cronjob/api';
Expand Down Expand Up @@ -206,11 +207,20 @@ const changeTrustOptHandler = new ChangeTrustOptHandler({
confirmationUIController,
});

const getStellarAccountActivationStatusHandler =
new GetStellarAccountActivationStatusHandler({
logger,
accountService,
networkService,
});

const clientRequestMethodHandlers: Record<
ClientRequestMethod,
IClientRequestHandler
> = {
[ClientRequestMethod.ChangeTrustOpt]: changeTrustOptHandler,
[ClientRequestMethod.GetStellarAccountActivationStatus]:
getStellarAccountActivationStatusHandler,
};

const clientRequestHandler = new ClientRequestHandler({
Expand Down
83 changes: 83 additions & 0 deletions packages/snap/src/handlers/clientRequest/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { assert, StructError } from '@metamask/superstruct';
import {
ChangeTrustOptJsonRpcRequestStruct,
ChangeTrustOptJsonRpcResponseStruct,
GetStellarAccountActivationStatusJsonRpcRequestStruct,
GetStellarAccountActivationStatusJsonRpcResponseStruct,
JsonRpcRequestWithAccountStruct,
} from './api';

Expand Down Expand Up @@ -219,3 +221,84 @@ describe('ChangeTrustOptJsonRpcRequestStruct', () => {
);
});
});

describe('GetStellarAccountActivationStatusJsonRpcRequestStruct', () => {
it('accepts a valid getStellarAccountActivationStatus JSON-RPC request', () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'getStellarAccountActivationStatus',
params: {
accountId,
scope,
},
};

expect(() =>
assert(request, GetStellarAccountActivationStatusJsonRpcRequestStruct),
).not.toThrow();
});

it.each([
{
jsonrpc: '2.0' as const,
id: 1,
method: 'wrongMethod',
params: {
accountId,
scope,
},
},
{
jsonrpc: '2.0' as const,
id: 1,
method: 'getStellarAccountActivationStatus',
params: {
accountId,
scope: 'stellar:unknown',
},
},
{
jsonrpc: '2.0' as const,
id: 1,
method: 'getStellarAccountActivationStatus',
params: {
accountId: 'invalid-account-id',
scope,
},
},
])(
'rejects an invalid getStellarAccountActivationStatus JSON-RPC request',
(request) => {
expect(() =>
assert(request, GetStellarAccountActivationStatusJsonRpcRequestStruct),
).toThrow(StructError);
},
);
});

describe('GetStellarAccountActivationStatusJsonRpcResponseStruct', () => {
it.each([{ activated: true }, { activated: false }])(
'accepts a valid getStellarAccountActivationStatus JSON-RPC response',
(response) => {
expect(() =>
assert(
response,
GetStellarAccountActivationStatusJsonRpcResponseStruct,
),
).not.toThrow();
},
);

it.each([{ activated: 'true' }, {}, { status: true }])(
'rejects an invalid getStellarAccountActivationStatus JSON-RPC response',
(response) => {
expect(() =>
assert(
response,
GetStellarAccountActivationStatusJsonRpcResponseStruct,
),
).toThrow(StructError);
},
);
});
38 changes: 38 additions & 0 deletions packages/snap/src/handlers/clientRequest/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
export enum ClientRequestMethod {
/** -------------------------------- Stellar Specific -------------------------------- */
ChangeTrustOpt = 'changeTrustOpt',
GetStellarAccountActivationStatus = 'getStellarAccountActivationStatus',
}

/**
Expand Down Expand Up @@ -109,6 +110,29 @@ export const ChangeTrustOptJsonRpcResponseStruct = object({
transactionId: optional(base64(string())),
});

const GetStellarAccountActivationStatusParamsStruct = object({
accountId: UuidStruct,
scope: KnownCaip2ChainIdStruct,
});

/**
* Validation struct for the getStellarAccountActivationStatus JSON-RPC request.
*/
export const GetStellarAccountActivationStatusJsonRpcRequestStruct = assign(
JsonRpcRequestStruct,
object({
method: literal(ClientRequestMethod.GetStellarAccountActivationStatus),
params: GetStellarAccountActivationStatusParamsStruct,
}),
);

/**
* Validation struct for the getStellarAccountActivationStatus JSON-RPC response.
*/
export const GetStellarAccountActivationStatusJsonRpcResponseStruct = object({
activated: boolean(),
});

/**
* A JSON-RPC request with an account resolve parameter.
*/
Expand All @@ -130,3 +154,17 @@ export type ChangeTrustOptJsonRpcRequest = Infer<
export type ChangeTrustOptJsonRpcResponse = Infer<
typeof ChangeTrustOptJsonRpcResponseStruct
>;

/**
* Type for the getStellarAccountActivationStatus JSON-RPC request.
*/
export type GetStellarAccountActivationStatusJsonRpcRequest = Infer<
typeof GetStellarAccountActivationStatusJsonRpcRequestStruct
>;

/**
* Type for the getStellarAccountActivationStatus JSON-RPC response.
*/
export type GetStellarAccountActivationStatusJsonRpcResponse = Infer<
typeof GetStellarAccountActivationStatusJsonRpcResponseStruct
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
ClientRequestMethod,
type GetStellarAccountActivationStatusJsonRpcRequest,
} from './api';
import { GetStellarAccountActivationStatusHandler } from './getStellarAccountActivationStatus';
import { KnownCaip2ChainId } from '../../api';
import { AccountService } from '../../services/account';
import { generateStellarKeyringAccount } from '../../services/account/__mocks__/account.fixtures';
import { NetworkService } from '../../services/network';
import { mockOnChainAccountService } from '../../services/on-chain-account/__mocks__/onChainAccount.fixtures';
import { getTestWallet } from '../../services/wallet/__mocks__/wallet.fixtures';
import { logger } from '../../utils/logger';

jest.mock('../../utils/logger');

describe('GetStellarAccountActivationStatusHandler', () => {
const accountId = '11111111-1111-4111-8111-111111111111';
const scope = KnownCaip2ChainId.Mainnet;

function buildRequest(): GetStellarAccountActivationStatusJsonRpcRequest {
return {
jsonrpc: '2.0',
id: 1,
method: ClientRequestMethod.GetStellarAccountActivationStatus,
params: { accountId, scope },
};
}

it('returns activated true when Horizon has the account', async () => {
const wallet = getTestWallet();
const account = generateStellarKeyringAccount(
accountId,
wallet.address,
'entropy-source-1',
0,
);

const resolveAccountSpy = jest
.spyOn(AccountService.prototype, 'resolveAccount')
.mockResolvedValue({ account });
const getAccountOrNullSpy = jest
.spyOn(NetworkService.prototype, 'getAccountOrNull')
.mockResolvedValue({} as never);

const { accountService } = mockOnChainAccountService();
const networkService = new NetworkService({ logger });
const handler = new GetStellarAccountActivationStatusHandler({
logger,
accountService,
networkService,
});

expect(await handler.handle(buildRequest())).toStrictEqual({
activated: true,
});

expect(resolveAccountSpy).toHaveBeenCalledWith({ accountId });
expect(getAccountOrNullSpy).toHaveBeenCalledWith(account.address, scope);
});

it('returns activated false when Horizon has no account', async () => {
const wallet = getTestWallet();
const account = generateStellarKeyringAccount(
accountId,
wallet.address,
'entropy-source-1',
0,
);

jest
.spyOn(AccountService.prototype, 'resolveAccount')
.mockResolvedValue({ account });
const getAccountOrNullSpy = jest
.spyOn(NetworkService.prototype, 'getAccountOrNull')
.mockResolvedValue(null);

const { accountService } = mockOnChainAccountService();
const networkService = new NetworkService({ logger });
const handler = new GetStellarAccountActivationStatusHandler({
logger,
accountService,
networkService,
});

expect(await handler.handle(buildRequest())).toStrictEqual({
activated: false,
});

expect(getAccountOrNullSpy).toHaveBeenCalledWith(account.address, scope);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type {
GetStellarAccountActivationStatusJsonRpcRequest,
GetStellarAccountActivationStatusJsonRpcResponse,
} from './api';
import {
GetStellarAccountActivationStatusJsonRpcRequestStruct,
GetStellarAccountActivationStatusJsonRpcResponseStruct,
} from './api';
import type { AccountService } from '../../services/account';
import type { NetworkService } from '../../services/network';
import type { ILogger } from '../../utils/logger';
import { createPrefixedLogger } from '../../utils/logger';
import { BaseHandler } from '../base';

export class GetStellarAccountActivationStatusHandler extends BaseHandler<
GetStellarAccountActivationStatusJsonRpcRequest,
GetStellarAccountActivationStatusJsonRpcResponse
> {
readonly #accountService: AccountService;

readonly #networkService: NetworkService;

constructor({
logger,
accountService,
networkService,
}: {
logger: ILogger;
accountService: AccountService;
networkService: NetworkService;
}) {
const prefixedLogger = createPrefixedLogger(
logger,
'[GetStellarAccountActivationStatusHandler]',
);
super({
logger: prefixedLogger,
requestStruct: GetStellarAccountActivationStatusJsonRpcRequestStruct,
responseStruct: GetStellarAccountActivationStatusJsonRpcResponseStruct,
});
this.#accountService = accountService;
this.#networkService = networkService;
}

/**
* Returns whether the account exists on Horizon for the requested network scope.
*
* @param request - JSON-RPC request with `accountId` and `scope`.
* @returns `{ activated: true }` when Horizon has the account; `false` when missing / not funded on-ledger.
*/
protected async handleRequest(
request: GetStellarAccountActivationStatusJsonRpcRequest,
): Promise<GetStellarAccountActivationStatusJsonRpcResponse> {
const { accountId, scope } = request.params;

const { account } = await this.#accountService.resolveAccount({
accountId,
});

const onChain = await this.#networkService.getAccountOrNull(
account.address,
scope,
);

return { activated: onChain !== null };
}
}
1 change: 1 addition & 0 deletions packages/snap/src/handlers/clientRequest/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './changeTrustOpt';
export * from './getStellarAccountActivationStatus';
export * from './clientRequest';
export * from './api';
export type { IClientRequestHandler } from './base';
Loading