Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added export state log feature #841

Merged
merged 5 commits into from
Oct 11, 2024
Merged
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
39 changes: 37 additions & 2 deletions packages/custodyController/src/custody.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CUSTODIAN_TYPES } from "@metamask-institutional/custody-keyring";
import { ITransactionStatusMap } from "@metamask-institutional/types";
import { IApiCallLogEntry, ITransactionStatusMap } from "@metamask-institutional/types";
import { ObservableStore } from "@metamask/obs-store";

import { CustodyAccountDetails } from "./types";
Expand All @@ -16,8 +16,9 @@ import { toChecksumHexAddress } from "./utils";
*/
export class CustodyController {
public store;

private readonly MAX_LOG_ENTRIES = 500;
public captureException: (e: Error) => void;

/**
* Creates a new controller instance
*
Expand All @@ -29,12 +30,46 @@ export class CustodyController {
this.store = new ObservableStore({
custodyAccountDetails: {} as { [key: string]: CustodyAccountDetails },
custodianConnectRequest: {},
apiRequestLogs: [],
...initState,
});

this.captureException = captureException;
}

storeApiCallLog(apiLogEntry: IApiCallLogEntry): void {
const { apiRequestLogs } = this.store.getState();

const updatedApiRequestLogs = apiRequestLogs ? [...apiRequestLogs] : [];

if (updatedApiRequestLogs.length >= this.MAX_LOG_ENTRIES) {
updatedApiRequestLogs.shift();
}

updatedApiRequestLogs.push(apiLogEntry);

this.store.updateState({ apiRequestLogs: updatedApiRequestLogs });
}

sanitizeAndLogApiCall(apiLogEntry: IApiCallLogEntry): void {
const { id, method, endpoint, success, timestamp, errorMessage, responseData } = apiLogEntry;

const sanitizedEntry: IApiCallLogEntry = {
id,
method,
endpoint,
success,
timestamp,
responseData: success ? responseData : undefined,
};

if (!success && errorMessage) {
sanitizedEntry.errorMessage = errorMessage;
}

this.storeApiCallLog(sanitizedEntry);
}

storeCustodyStatusMap(custody: string, custodyStatusMap: ITransactionStatusMap): void {
try {
const { custodyStatusMaps } = this.store.getState();
Expand Down
8 changes: 8 additions & 0 deletions packages/custodyKeyring/src/CustodyKeyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AddressType,
AuthDetails,
AuthTypes,
IApiCallLogEntry,
ICustodianAccount,
ICustodianTransactionLink,
ICustodianType,
Expand All @@ -25,6 +26,7 @@ import crypto from "crypto";
import { EventEmitter } from "events";

import {
API_REQUEST_LOG_EVENT,
DEFAULT_MAX_CACHE_AGE,
INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT,
REFRESH_TOKEN_CHANGE_EVENT,
Expand Down Expand Up @@ -249,6 +251,10 @@ export abstract class CustodyKeyring extends EventEmitter {
this.emit(INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, event); // Propagate the event to the extension where it calls for the keyrings to be persisted
}

emitApiRequestLogEvent(event: IApiCallLogEntry): void {
this.emit(API_REQUEST_LOG_EVENT, event);
}

createAuthDetails(token: string): AuthDetails {
let authDetails: AuthDetails;

Expand Down Expand Up @@ -287,6 +293,8 @@ export abstract class CustodyKeyring extends EventEmitter {
this.handleInteractiveRefreshTokenChangeEvent(event),
);

sdk.on(API_REQUEST_LOG_EVENT, (event: IApiCallLogEntry) => this.emitApiRequestLogEvent(event));

this.sdkList.push({
sdk,
hash,
Expand Down
1 change: 1 addition & 0 deletions packages/custodyKeyring/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const DEFAULT_MAX_CACHE_AGE = 60;
export const REFRESH_TOKEN_CHANGE_EVENT = "refresh_token_change";
export const INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT = "interactive_replacement_token_change";
export const API_REQUEST_LOG_EVENT = "API_REQUEST_LOG_EVENT";
11 changes: 10 additions & 1 deletion packages/sdk/src/classes/MMISDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SimpleCache } from "@metamask-institutional/simplecache";
import {
AuthDetails,
AuthTypes,
IApiCallLogEntry,
ICustodianTransactionLink,
IEIP1559TxParams,
ILegacyTXParams,
Expand All @@ -16,7 +17,11 @@ import { CustodianApiConstructor, ICustodianApi } from "src/interfaces/ICustodia
import { SignedMessageMetadata } from "src/types/SignedMessageMetadata";
import { SignedTypedMessageMetadata } from "src/types/SignedTypedMessageMetadata";

import { INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, REFRESH_TOKEN_CHANGE_EVENT } from "../constants/constants";
import {
API_REQUEST_LOG_EVENT,
INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT,
REFRESH_TOKEN_CHANGE_EVENT,
} from "../constants/constants";
import { IEthereumAccount } from "../interfaces/IEthereumAccount";
import { IEthereumAccountCustodianDetails } from "../interfaces/IEthereumAccountCustodianDetails";
import { MessageTypes, TypedMessage } from "../interfaces/ITypedMessage";
Expand Down Expand Up @@ -49,6 +54,10 @@ export class MMISDK extends EventEmitter {
this.custodianApi.on(INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, event => {
this.emit(INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, event);
});

this.custodianApi.on(API_REQUEST_LOG_EVENT, (event: IApiCallLogEntry) => {
this.emit(API_REQUEST_LOG_EVENT, event);
});
}

// Do an in-situ replacement of the auth details
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const REFRESH_TOKEN_CHANGE_EVENT = "refresh_token_change";
export const INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT = "interactive_replacement_token_change";
export const DEFAULT_MAX_CACHE_AGE = 60;
export const API_REQUEST_LOG_EVENT = "API_REQUEST_LOG_EVENT";
23 changes: 21 additions & 2 deletions packages/sdk/src/custodianApi/eca3/ECA3Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { IRefreshTokenChangeEvent } from "@metamask-institutional/types";
import crypto from "crypto";
import { EventEmitter } from "events";

import { INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, REFRESH_TOKEN_CHANGE_EVENT } from "../../constants/constants";
import {
API_REQUEST_LOG_EVENT,
INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT,
REFRESH_TOKEN_CHANGE_EVENT,
} from "../../constants/constants";
import { JsonRpcResult } from "./interfaces/JsonRpcResult";
import { JsonRpcCreateTransactionPayload } from "./rpc-payloads/JsonRpcCreateTransactionPayload";
import { JsonRpcGetSignedMessageByIdPayload } from "./rpc-payloads/JsonRpcGetSignedMessageByIdPayload";
Expand Down Expand Up @@ -38,7 +42,7 @@ export class ECA3Client extends EventEmitter {
constructor(private apiBaseUrl: string, private refreshToken: string, private refreshTokenUrl: string) {
super();

this.call = factory(`${apiBaseUrl}/v3/json-rpc`);
this.call = factory(`${this.apiBaseUrl}/v3/json-rpc`, this.emit.bind(this));

this.cache = new SimpleCache();
}
Expand Down Expand Up @@ -132,8 +136,23 @@ export class ECA3Client extends EventEmitter {
this.emit(REFRESH_TOKEN_CHANGE_EVENT, payload);
}

this.emit(API_REQUEST_LOG_EVENT, {
method: "POST",
endpoint: this.refreshTokenUrl,
success: response.ok,
timestamp: new Date().toISOString(),
errorMessage: response.ok ? undefined : responseJson.message,
});

return responseJson.access_token;
} catch (error) {
this.emit(API_REQUEST_LOG_EVENT, {
method: "POST",
endpoint: this.refreshTokenUrl,
success: false,
timestamp: new Date().toISOString(),
errorMessage: error.message,
});
throw new Error(`Error getting the Access Token: ${error}`);
}
}
Expand Down
14 changes: 10 additions & 4 deletions packages/sdk/src/custodianApi/eca3/ECA3CustodianApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SimpleCache } from "@metamask-institutional/simplecache";
import {
AuthTypes,
IApiCallLogEntry,
ICustodianTransactionLink,
IEIP1559TxParams,
ILegacyTXParams,
Expand All @@ -11,12 +12,14 @@ import {
} from "@metamask-institutional/types";
import { EventEmitter } from "events";
import { SignedMessageMetadata } from "src/types/SignedMessageMetadata";
import { SignedMessageParams } from "src/types/SignedMessageParams";
import { SignedTypedMessageMetadata } from "src/types/SignedTypedMessageMetadata";
import { SignedTypedMessageParams } from "src/types/SignedTypedMessageParams";

import { AccountHierarchyNode } from "../../classes/AccountHierarchyNode";
import { INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, REFRESH_TOKEN_CHANGE_EVENT } from "../../constants/constants";
import {
API_REQUEST_LOG_EVENT,
INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT,
REFRESH_TOKEN_CHANGE_EVENT,
} from "../../constants/constants";
import { ICustodianApi } from "../../interfaces/ICustodianApi";
import { IEthereumAccount } from "../../interfaces/IEthereumAccount";
import { IEthereumAccountCustodianDetails } from "../../interfaces/IEthereumAccountCustodianDetails";
Expand All @@ -25,7 +28,6 @@ import { CreateTransactionMetadata } from "../../types/CreateTransactionMetadata
import { ECA3Client } from "./ECA3Client";
import { JsonRpcTransactionParams } from "./rpc-payloads/JsonRpcCreateTransactionPayload";
import { JsonRpcReplaceTransactionParams } from "./rpc-payloads/JsonRpcReplaceTransactionPayload";
import { JsonRpcListAccountsSignedResponse } from "./rpc-responses/JsonRpcListAccountsSignedResponse";
import { hexlify } from "./util/hexlify";
import { mapStatusObjectToStatusText } from "./util/mapStatusObjectToStatusText";

Expand Down Expand Up @@ -55,6 +57,10 @@ export class ECA3CustodianApi extends EventEmitter implements ICustodianApi {
this.client.on(INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, event => {
this.emit(INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, event);
});

this.client.on(API_REQUEST_LOG_EVENT, (event: IApiCallLogEntry) => {
this.emit(API_REQUEST_LOG_EVENT, event);
});
}

getAccountHierarchy(): Promise<AccountHierarchyNode> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("json-rpc-call", () => {
describe("json-rpc-call", () => {
it("should call the JSON RPC endpoint with the appropriate method and parameters", async () => {
fetchMock.mockResponseOnce(JSON.stringify({ result: "test" }));
const call = factory("http://test/json-rpc");
const call = factory("http://test/json-rpc", jest.fn());

await call("test", { some: "parameter" }, "access_token");

Expand All @@ -37,7 +37,7 @@ describe("json-rpc-call", () => {
}),
);

const call = factory("http://test/json-rpc");
const call = factory("http://test/json-rpc", jest.fn());

await expect(call("test", { some: "parameter" }, "access_token")).rejects.toThrow("Test error");
});
Expand Down
25 changes: 24 additions & 1 deletion packages/sdk/src/custodianApi/eca3/util/json-rpc-call.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { IApiCallLogEntry } from "@metamask-institutional/types";

import { API_REQUEST_LOG_EVENT } from "../../../constants/constants";
import { JsonRpcError } from "../interfaces/JsonRpcError";
import { JsonRpcResult } from "../interfaces/JsonRpcResult";

export default function (jsonRpcEndpoint: string) {
export default function (jsonRpcEndpoint: string, emit: (eventName: string, eventData: IApiCallLogEntry) => void) {
let requestId = 0;

return async function jsonRpcCall<T1, T2>(method: string, params: T1, accessToken: string): Promise<T2> {
Expand Down Expand Up @@ -30,6 +33,16 @@ export default function (jsonRpcEndpoint: string) {

responseJson = await response.json();

emit(API_REQUEST_LOG_EVENT, {
id: requestId,
method,
endpoint: jsonRpcEndpoint,
success: !responseJson.error,
timestamp: new Date().toISOString(),
errorMessage: responseJson.error ? responseJson.error.message : undefined,
responseData: responseJson.result,
});

if ((responseJson as JsonRpcError).error) {
console.log("JSON-RPC <", method, requestId, responseJson, jsonRpcEndpoint);
throw new Error((responseJson as JsonRpcError).error.message);
Expand All @@ -42,6 +55,16 @@ export default function (jsonRpcEndpoint: string) {

console.log("JSON-RPC <", method, requestId, e, jsonRpcEndpoint);

emit(API_REQUEST_LOG_EVENT, {
id: requestId,
method,
endpoint: jsonRpcEndpoint,
success: false,
timestamp: new Date().toISOString(),
errorMessage: e.message,
responseData: null,
});

throw e;
}

Expand Down
23 changes: 21 additions & 2 deletions packages/sdk/src/custodianApi/json-rpc/JsonRpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { IRefreshTokenChangeEvent } from "@metamask-institutional/types";
import crypto from "crypto";
import { EventEmitter } from "events";

import { INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, REFRESH_TOKEN_CHANGE_EVENT } from "../../constants/constants";
import {
API_REQUEST_LOG_EVENT,
INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT,
REFRESH_TOKEN_CHANGE_EVENT,
} from "../../constants/constants";
import { JsonRpcResult } from "./interfaces/JsonRpcResult";
import { JsonRpcCreateTransactionPayload } from "./rpc-payloads/JsonRpcCreateTransactionPayload";
import { JsonRpcGetSignedMessageByIdPayload } from "./rpc-payloads/JsonRpcGetSignedMessageByIdPayload";
Expand Down Expand Up @@ -33,7 +37,7 @@ export class JsonRpcClient extends EventEmitter {
constructor(private apiBaseUrl: string, private refreshToken: string, private refreshTokenUrl: string) {
super();

this.call = factory(`${apiBaseUrl}/v1/json-rpc`);
this.call = factory(`${this.apiBaseUrl}/v1/json-rpc`, this.emit.bind(this));

this.cache = new SimpleCache();
}
Expand Down Expand Up @@ -127,8 +131,23 @@ export class JsonRpcClient extends EventEmitter {
this.emit(REFRESH_TOKEN_CHANGE_EVENT, payload);
}

this.emit(API_REQUEST_LOG_EVENT, {
method: "POST",
endpoint: this.refreshTokenUrl,
success: response.ok,
timestamp: new Date().toISOString(),
errorMessage: response.ok ? undefined : responseJson.message,
});

return responseJson.access_token;
} catch (error) {
this.emit(API_REQUEST_LOG_EVENT, {
method: "POST",
endpoint: this.refreshTokenUrl,
success: false,
timestamp: new Date().toISOString(),
errorMessage: error.message,
});
throw new Error(`Error getting the Access Token: ${error}`);
}
}
Expand Down
11 changes: 10 additions & 1 deletion packages/sdk/src/custodianApi/json-rpc/JsonRpcCustodianApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SimpleCache } from "@metamask-institutional/simplecache";
import {
AuthTypes,
IApiCallLogEntry,
ICustodianTransactionLink,
IEIP1559TxParams,
ILegacyTXParams,
Expand All @@ -12,7 +13,11 @@ import {
import { EventEmitter } from "events";

import { AccountHierarchyNode } from "../../classes/AccountHierarchyNode";
import { INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, REFRESH_TOKEN_CHANGE_EVENT } from "../../constants/constants";
import {
API_REQUEST_LOG_EVENT,
INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT,
REFRESH_TOKEN_CHANGE_EVENT,
} from "../../constants/constants";
import { ICustodianApi } from "../../interfaces/ICustodianApi";
import { IEthereumAccount } from "../../interfaces/IEthereumAccount";
import { IEthereumAccountCustodianDetails } from "../../interfaces/IEthereumAccountCustodianDetails";
Expand Down Expand Up @@ -49,6 +54,10 @@ export class JsonRpcCustodianApi extends EventEmitter implements ICustodianApi {
this.client.on(INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, event => {
this.emit(INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, event);
});

this.client.on(API_REQUEST_LOG_EVENT, (event: IApiCallLogEntry) => {
this.emit(API_REQUEST_LOG_EVENT, event);
});
}

getAccountHierarchy(): Promise<AccountHierarchyNode> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("json-rpc-call", () => {
describe("json-rpc-call", () => {
it("should call the JSON RPC endpoint with the appropriate method and parameters", async () => {
fetchMock.mockResponseOnce(JSON.stringify({ result: "test" }));
const call = factory("http://test/json-rpc");
const call = factory("http://test/json-rpc", jest.fn());

await call("test", { some: "parameter" }, "access_token");

Expand All @@ -37,7 +37,7 @@ describe("json-rpc-call", () => {
}),
);

const call = factory("http://test/json-rpc");
const call = factory("http://test/json-rpc", jest.fn());

await expect(call("test", { some: "parameter" }, "access_token")).rejects.toThrow("Test error");
});
Expand Down
Loading
Loading