Skip to content

Commit

Permalink
TRM on connect (#945)
Browse files Browse the repository at this point in the history
* TRM on connect

* prettier
  • Loading branch information
danielisaacgeslin authored Jul 25, 2024
1 parent 9cebe51 commit 2e31919
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 0 deletions.
2 changes: 2 additions & 0 deletions apps/connect/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { PrivacyPolicyPath, isPreview, isProduction } from "./utils/constants";
import Banner from "./components/atoms/Banner";
import { ENV } from "@env";
import { clearUrl, pushResumeUrl } from "./navs/navs";
import { validateTransferHandler } from "./providers/sanctions";

const defaultConfig: WormholeConnectConfig = {
...ENV.wormholeConnectConfig,
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function Root() {
const config: ComponentProps<typeof WormholeConnect>["config"] = useMemo(
() => ({
...defaultConfig,
validateTransferHandler,
searchTx: {
...(txHash && { txHash }),
...(sourceChain && { chainName: sourceChain }),
Expand Down
128 changes: 128 additions & 0 deletions apps/connect/src/providers/sanctions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
ACCOUNT_ID,
RISK_ADDRESS_INDICATOR_TYPE,
RISK_LEVEL_SANCTION,
SanctionResponse,
TRM_URL,
validateTransferHandler,
} from "./sanctions";

describe("sanctions", () => {
let transferDetails: Parameters<typeof validateTransferHandler>[0];
let validResponse: SanctionResponse;
beforeEach(() => {
global.fetch = jest.fn().mockResolvedValue({ json: jest.fn() });
transferDetails = {
fromChain: "fromChain",
fromWalletAddress: "fromWalletAddress",
toChain: "toChain",
toWalletAddress: "toWalletAddress",
} as any;
validResponse = {
addressRiskIndicators: [
{
categoryRiskScoreLevel: RISK_LEVEL_SANCTION - 1,
riskType: RISK_ADDRESS_INDICATOR_TYPE,
},
],
entities: [{ riskScoreLevel: RISK_LEVEL_SANCTION - 1 }],
};
});

it("should be valid when no address is sanctioned", async () => {
global.fetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue([validResponse]),
});
expect(await validateTransferHandler(transferDetails)).toEqual({
isValid: true,
});
expect(global.fetch).toHaveBeenNthCalledWith(
1,
TRM_URL,
expect.objectContaining({
body: JSON.stringify([
{
address: transferDetails.fromWalletAddress,
chain: transferDetails.fromChain,
accountExternalId: ACCOUNT_ID,
},
]),
})
);
expect(global.fetch).toHaveBeenNthCalledWith(
2,
TRM_URL,
expect.objectContaining({
body: JSON.stringify([
{
address: transferDetails.toWalletAddress,
chain: transferDetails.toChain,
accountExternalId: ACCOUNT_ID,
},
]),
})
);
});

it("should be valid when api fails", async () => {
console.error = jest.fn();
global.fetch = jest.fn().mockRejectedValue({});
expect(await validateTransferHandler(transferDetails)).toEqual({
isValid: true,
});
});

it("should be valid when api returns unexpected data", async () => {
console.error = jest.fn();
global.fetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue({ random: true }),
});

expect(await validateTransferHandler(transferDetails)).toEqual({
isValid: true,
});
});

it("should NOT be valid when one address is NOT valid because of the address risk level", async () => {
const json = jest
.fn()
.mockResolvedValueOnce([validResponse])
.mockResolvedValueOnce([
{
...validResponse,
addressRiskIndicators: [
...validResponse.addressRiskIndicators,
{
categoryRiskScoreLevel: RISK_LEVEL_SANCTION,
riskType: RISK_ADDRESS_INDICATOR_TYPE,
},
],
},
]);
global.fetch = jest.fn().mockResolvedValue({ json });

expect(await validateTransferHandler(transferDetails)).toEqual({
isValid: false,
});
});

it("should NOT be valid when one address is NOT valid because of the entity risk level", async () => {
const json = jest
.fn()
.mockResolvedValueOnce([validResponse])
.mockResolvedValueOnce([
{
...validResponse,
entities: [
...validResponse.entities,
{ riskScoreLevel: RISK_LEVEL_SANCTION },
],
},
]);
global.fetch = jest.fn().mockResolvedValue({ json });

expect(await validateTransferHandler(transferDetails)).toEqual({
isValid: false,
});
});
});
59 changes: 59 additions & 0 deletions apps/connect/src/providers/sanctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { WormholeConnectConfig } from "@wormhole-foundation/wormhole-connect";

export interface SanctionResponse {
addressRiskIndicators: { categoryRiskScoreLevel: number; riskType: string }[];
entities: { riskScoreLevel: number }[];
}

export const TRM_URL =
"https://hjukqn406c.execute-api.us-east-2.amazonaws.com/addresses";
export const ACCOUNT_ID = "PortalBridge";
export const RISK_LEVEL_SANCTION: number = 10;
export const RISK_ADDRESS_INDICATOR_TYPE = "OWNERSHIP";

const isSanctioned = async ({
chain,
address,
}: {
chain: string;
address: string;
}) => {
try {
const [response]: SanctionResponse[] = await fetch(TRM_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([{ address, chain, accountExternalId: ACCOUNT_ID }]),
}).then((r) => r.json());

return !!(
response?.addressRiskIndicators?.some(
(risk) =>
risk?.riskType === RISK_ADDRESS_INDICATOR_TYPE &&
risk?.categoryRiskScoreLevel >= RISK_LEVEL_SANCTION
) ||
response?.entities?.some(
(entity) => entity?.riskScoreLevel >= RISK_LEVEL_SANCTION
)
);
} catch (error) {
console.error("Error validating transfer", { chain, address, error });
return false;
}
};

export const validateTransferHandler: NonNullable<
WormholeConnectConfig["validateTransferHandler"]
> = async (transferDetails) => {
const [isOriginSanctioned, isTargetSanctioned] = await Promise.all([
isSanctioned({
chain: transferDetails.fromChain,
address: transferDetails.fromWalletAddress,
}),
isSanctioned({
chain: transferDetails.toChain,
address: transferDetails.toWalletAddress,
}),
]);

return { isValid: !isOriginSanctioned && !isTargetSanctioned };
};

0 comments on commit 2e31919

Please sign in to comment.