diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index adba0006..e2d216b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,7 +30,8 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} INFURA_PROJECT_ID: ${{ secrets.INFURA_PROJECT_ID }} ALCHEMY_KEY: ${{ secrets.ALCHEMY_KEY }} - SANCTIONS_API_KEY: ${{ secrets.SANCTIONS_API_KEY }} + HYPERNATIVE_EMAIL: ${{ secrets.HYPERNATIVE_EMAIL }} + HYPERNATIVE_PASSWORD: ${{ secrets.HYPERNATIVE_PASSWORD }} TENDERLY_USER: ${{ secrets.TENDERLY_USER }} TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }} TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }} diff --git a/README.md b/README.md index 6d28fb2a..b665d60b 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,6 @@ You can customize your deployment with env variables. See .env.example for all p #### Additional Settings - Rarely used - DOMAIN_NAME - The domain that API Gateway will run on. If specified a random AWS domain will be created. -- SANCTIONS_API_KEY - TRM API key for running sanction checks. - TENDERLY_USER - Your Tenderly user id, used by the `/tenderly` endpoints. - TENDERLY_PROJECT - Your Tenderly project id, used by the `/tenderly` endpoints. - TENDERLY_ACCESS_KEY - Your tenderly access key, used by the `/tenderly` endpoints. diff --git a/index.ts b/index.ts index b73e49dd..92f054b5 100644 --- a/index.ts +++ b/index.ts @@ -49,7 +49,8 @@ const { UPDATE_POOLS_INTERVAL_IN_MINUTES, DECORATE_POOLS_INTERVAL_IN_MINUTES, DOMAIN_NAME, - SANCTIONS_API_KEY, + HYPERNATIVE_EMAIL, + HYPERNATIVE_PASSWORD, NETWORKS, TENDERLY_USER, TENDERLY_PROJECT, @@ -373,7 +374,8 @@ export class BalancerPoolsAPI extends Stack { const checkWalletLambda = new NodejsFunction(this, 'checkWalletFunction', { entry: join(__dirname, 'src', 'lambdas', 'check-wallet.ts'), environment: { - SANCTIONS_API_KEY: SANCTIONS_API_KEY || '', + HYPERNATIVE_EMAIL: HYPERNATIVE_EMAIL || '', + HYPERNATIVE_PASSWORD: HYPERNATIVE_PASSWORD || '', }, runtime: Runtime.NODEJS_14_X, timeout: Duration.seconds(15), diff --git a/scripts/test.ts b/scripts/test.ts index 0e18abe0..0ccf5218 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -1,11 +1,15 @@ console.log('Running test.ts!'); -import { fetchPoolsFromChain } from '../src/modules/chain-data/onchain'; +import { handler } from '../src/lambdas/check-wallet'; (async () => { try { console.log('Fetching pools...'); - const pools = await fetchPoolsFromChain(1); - console.log('Fetched', pools.length, 'pools'); + const response = await handler({ + queryStringParameters: { + address: '0x7f367cc41522ce07553e823bf3be79a889debe1b', + }, + }); + console.log('handler response:', response); } catch (e) { console.log(e); } diff --git a/src/lambdas/check-wallet.spec.ts b/src/lambdas/check-wallet.spec.ts index f0d12b41..1d88f3d3 100644 --- a/src/lambdas/check-wallet.spec.ts +++ b/src/lambdas/check-wallet.spec.ts @@ -1,100 +1,80 @@ import nock from 'nock'; import { handler } from './check-wallet'; -import { TRMAccountDetails } from '@/modules/trm'; nock.disableNetConnect(); -let trmResponse: TRMAccountDetails[] = []; - const request = { queryStringParameters: { address: '0x0000000000000000000000000000000000000000', }, }; +const goodResponse = { + data: [ + { + address: '0x0000000000000000000000000000000000000000', + recommendation: 'Approve', + flags: [], + }, + ], +}; + +const badResponse = { + data: [ + { + address: '0x0000000000000000000000000000000000000000', + recommendation: 'Deny', + flags: [ + { + title: 'Blacklisted by circle (USDC)', + valence: 'Negative', + flagId: 'F-1401', + chain: 'ethereum', + lastUpdate: '2018-12-28T15:36:24Z', + events: [Array], + }, + { + title: 'Received from OFAC-sanctioned', + valence: 'Negative', + flagId: 'F-1102', + chain: 'gnosis', + lastUpdate: '2024-03-19T22:06:55Z', + events: [Array], + }, + { + title: 'Sent to OFAC-sanctioned', + valence: 'Negative', + flagId: 'F-1103', + chain: 'gnosis', + lastUpdate: '2024-01-22T09:57:45Z', + events: [Array], + }, + ], + }, + ], +}; + describe('Wallet Check Lambda', () => { it('Should return false for an address with no issues', async () => { - trmResponse = [ - { - accountExternalId: null, - address: '0x0000000000000000000000000000000000000000', - addressRiskIndicators: [], - addressSubmitted: '0x0000000000000000000000000000000000000000', - chain: 'ethereum', - entities: [], - trmAppUrl: - 'https://my.trmlabs.com/address/0x0000000000000000000000000000000000000000/eth', - }, - ]; - nock('https://api.trmlabs.com') - .post('/public/v2/screening/addresses') - .reply(200, trmResponse); + nock('https://api.hypernative.xyz') + .post('/auth/login') + .reply(200, { data: { token: 'token' } }); + nock('https://api.hypernative.xyz') + .post('/assets/reputation/addresses') + .reply(200, goodResponse); const response = await handler(request); + console.log('Response:', response); const body = JSON.parse(response.body); expect(body.is_blocked).toBe(false); }); it('Should return blocked for an address that has a Severe risk', async () => { - trmResponse = [ - { - accountExternalId: null, - address: '0x0000000000000000000000000000000000000000', - addressRiskIndicators: [ - { - category: 'Sanctions', - categoryId: '69', - categoryRiskScoreLevel: 15, - categoryRiskScoreLevelLabel: 'Severe', - incomingVolumeUsd: - '570037717.3324722239717737602882028941260267337', - outgoingVolumeUsd: - '573357789.82550046115536928143858188991303929335', - riskType: 'OWNERSHIP', - totalVolumeUsd: '1143395507.15797268512714304172678478403906602705', - }, - { - category: 'Sanctions', - categoryId: '69', - categoryRiskScoreLevel: 15, - categoryRiskScoreLevelLabel: 'Severe', - incomingVolumeUsd: '0', - outgoingVolumeUsd: - '428172699.46703217102356766551565669942647220227', - riskType: 'COUNTERPARTY', - totalVolumeUsd: '428172699.46703217102356766551565669942647220227', - }, - ], - addressSubmitted: '0x0000000000000000000000000000000000000000', - chain: 'ethereum', - entities: [ - { - category: 'Sanctions', - categoryId: '69', - entity: 'Lazarus Group', - riskScoreLevel: 15, - riskScoreLevelLabel: 'Severe', - trmAppUrl: - 'https://my.trmlabs.com/entities/trm/75624c42-157e-4a63-8f32-070b1d1fa4d4', - trmUrn: '/entity/manual/75624c42-157e-4a63-8f32-070b1d1fa4d4', - }, - { - category: 'Hacked or Stolen Funds', - categoryId: '34', - entity: 'Ronin Bridge Hack - March 2022', - riskScoreLevel: 15, - riskScoreLevelLabel: 'Severe', - trmAppUrl: - 'https://my.trmlabs.com/entities/trm/9145c0ff-1544-475f-8403-40840cb051e0', - trmUrn: '/entity/manual/9145c0ff-1544-475f-8403-40840cb051e0', - }, - ], - trmAppUrl: - 'https://my.trmlabs.com/address/0x0000000000000000000000000000000000000000/eth', - }, - ]; - nock('https://api.trmlabs.com') - .post('/public/v2/screening/addresses') - .reply(200, trmResponse); + nock('https://api.hypernative.xyz') + .post('/auth/login') + .reply(200, { data: { token: 'token' } }); + nock('https://api.hypernative.xyz') + .post('/assets/reputation/addresses') + .reply(200, badResponse); const response = await handler(request); const body = JSON.parse(response.body); expect(body.is_blocked).toBe(true); diff --git a/src/lambdas/check-wallet.ts b/src/lambdas/check-wallet.ts index 6ffdf632..16fabf1a 100644 --- a/src/lambdas/check-wallet.ts +++ b/src/lambdas/check-wallet.ts @@ -1,12 +1,36 @@ import { wrapHandler } from '@/modules/sentry'; import { captureException } from '@sentry/serverless'; import fetch from 'isomorphic-fetch'; -import { TRMAccountDetails, TRMEntity, TRMRiskIndicator } from '@/modules/trm'; import { formatResponse } from './utils'; -const SANCTIONS_ENDPOINT = - 'https://api.trmlabs.com/public/v2/screening/addresses'; -const { SANCTIONS_API_KEY } = process.env; +const { HYPERNATIVE_EMAIL, HYPERNATIVE_PASSWORD } = process.env; + +type ReputationResponse = { + data: Array<{ flags: string[]; address: string; recommendation: string }>; +}; + +async function getAuthKey(): Promise { + try { + const res = await fetch('https://api.hypernative.xyz/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: HYPERNATIVE_EMAIL || '', + password: HYPERNATIVE_PASSWORD || '', + }), + }); + const { + data: { token }, + } = await res.json(); + + return token; + } catch (err) { + captureException(err); + return null; + } +} export const handler = wrapHandler(async (event: any = {}): Promise => { const address = event.queryStringParameters.address; @@ -17,39 +41,30 @@ export const handler = wrapHandler(async (event: any = {}): Promise => { ); } + const apiKey = await getAuthKey(); + if (!apiKey) return formatResponse(500, 'Unable to perform sanctions check'); + try { - const response = await fetch(SANCTIONS_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: - 'Basic ' + - Buffer.from(`${SANCTIONS_API_KEY}:${SANCTIONS_API_KEY}`).toString( - 'base64' - ), - }, - body: JSON.stringify([ - { - address: address.toLowerCase(), - chain: 'ethereum', + const response = await fetch( + 'https://api.hypernative.xyz/assets/reputation/addresses', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, }, - ]), - }); - - const result: TRMAccountDetails[] = await response.json(); - - const riskIndicators: TRMRiskIndicator[] = - result[0]?.addressRiskIndicators || []; - const entities: TRMEntity[] = result[0]?.entities || []; - - const hasSevereRisk = riskIndicators.some( - indicator => indicator.categoryRiskScoreLevelLabel === 'Severe' - ); - const hasSevereEntity = entities.some( - entity => entity.riskScoreLevelLabel === 'Severe' + body: JSON.stringify({ + addresses: [address], + expandDetails: true, + }), + } ); - const isBlocked = hasSevereEntity || hasSevereRisk; + const { + data: [check], + }: ReputationResponse = await response.json(); + + const isBlocked = check.recommendation === 'Deny'; return formatResponse( 200, @@ -61,7 +76,7 @@ export const handler = wrapHandler(async (event: any = {}): Promise => { console.log( `Received error performing wallet check on address ${address}: ${e}` ); - captureException(e, { extra: { address } }) + captureException(e, { extra: { address } }); return formatResponse(500, 'Unable to perform sanctions check'); } });