-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
3 changed files
with
189 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; |