Skip to content

Commit

Permalink
Feature/scan asset balances (#127)
Browse files Browse the repository at this point in the history
* add isMalicious field to balances

* default to setting all balances to isMalicious: false

* rm comment

* update test name

* fix formatting
  • Loading branch information
piyalbasu authored and aristidesstaffieri committed Aug 15, 2024
1 parent 5ea726d commit 26bd893
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 0 deletions.
88 changes: 88 additions & 0 deletions src/helper/test-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ function backendClientMaker(network: NetworkNames) {
error: null,
});
}
case query.getTokenBalanceSub(
"CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ",
tokenBalanceLedgerKey
): {
return Promise.resolve({
data: {
allEntryUpdates: {
nodes: [
{
contractId:
"CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ",
},
],
},
},
error: null,
});
}
case query.getCurrentDataAccountBalances(
pubKey,
tokenBalanceLedgerKey,
Expand Down Expand Up @@ -170,6 +188,18 @@ function backendClientMaker(network: NetworkNames) {
error: null,
});
}
case query.getCurrentDataAccountBalances(pubKey, tokenBalanceLedgerKey, [
"CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP",
"CBGTG7XFRY3L6OKAUTR6KGDKUXUQBX3YDJ3QFDYTGVMOM7VV4O7NCODG",
"CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ",
]): {
return Promise.resolve({
data: queryMockResponse[
"query.getAccountBalancesCurrentDataWithThreeContracts"
],
error: null,
});
}
case query.getAccountObject(pubKey): {
return Promise.resolve({
data: queryMockResponse["query.getAccountObject"],
Expand Down Expand Up @@ -307,6 +337,51 @@ const queryMockResponse = {
},
],
},
"query.getAccountBalancesCurrentDataWithThreeContracts": {
trustlinesByPublicKey: [
{
balance: 100019646386,
asset: "AAAAAUJMTkQAAAAAJgXM07IdPwaDCLLNw46HAu0Jy3Az9GJKesWnsk57zF4=",
limit: 1,
accountId: pubKey,
},
],
accountByPublicKey: {
accountId: pubKey,
nativeBalance: "10",
numSubEntries: "1",
numSponsored: "1",
numSponsoring: "1",
sellingLiabilities: "1000000",
},
CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP: [
{
contractId: "CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP",
keyXdr:
"AAAAEAAAAAEAAAACAAAADwAAAAdCYWxhbmNlAAAAABIAAAAAAAAAAIzohH0YeJGhVUA7vwkk2SzLZ8oNA2zaMGfxtpyxEtys",
valXdr: contractDataEntryValXdr,
durability: 1,
},
],
CBGTG7XFRY3L6OKAUTR6KGDKUXUQBX3YDJ3QFDYTGVMOM7VV4O7NCODG: [
{
contractId: "CBGTG7XFRY3L6OKAUTR6KGDKUXUQBX3YDJ3QFDYTGVMOM7VV4O7NCODG",
keyXdr:
"AAAAEAAAAAEAAAACAAAADwAAAAdCYWxhbmNlAAAAABIAAAAAAAAAAIzohH0YeJGhVUA7vwkk2SzLZ8oNA2zaMGfxtpyxEtys",
valXdr: contractDataEntryValXdr,
durability: 1,
},
],
CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ: [
{
contractId: "CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ",
keyXdr:
"AAAAEAAAAAEAAAACAAAADwAAAAdCYWxhbmNlAAAAABIAAAAAAAAAAIzohH0YeJGhVUA7vwkk2SzLZ8oNA2zaMGfxtpyxEtys",
valXdr: contractDataEntryValXdr,
durability: 1,
},
],
},
"query.getAccountObject": {
accountObjectByPublicKey: {
nodes: [
Expand Down Expand Up @@ -350,6 +425,19 @@ const queryMockResponse = {
},
],
},
CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ: {
nodes: [
{
contractId:
"CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ",
keyXdr: tokenBalanceLedgerKey,
valueXdr,
ledgerTimestamp: "timestamp",
ledger: "1",
entryDurability: "persistent",
},
],
},
balanceByPublicKey: {
nodes: [],
},
Expand Down
111 changes: 111 additions & 0 deletions src/route/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "@blockaid/client";
import {
getDevServer,
queryMockResponse,
Expand All @@ -7,6 +8,28 @@ import {
import { transformAccountHistory } from "../service/mercury/helpers/transformers";
import { query } from "../service/mercury/queries";

jest.mock("@blockaid/client", () => {
return class Blockaid {
token = {
scan: (asset: { address: string; chain: string }) => {
if (
asset.address ===
"BLND-GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56"
) {
return Promise.resolve({ malicious_score: 1 });
}
if (
asset.address ===
"TST-CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ"
) {
throw Error("ERROR");
}
return Promise.resolve({ malicious_score: 0 });
},
};
};
});

describe("API routes", () => {
describe("/account-history/:pubKey", () => {
it("can fetch an account history for a pub key", async () => {
Expand Down Expand Up @@ -123,5 +146,93 @@ describe("API routes", () => {
register.clear();
await server.close();
});

it("adds scanned status on Pubnet", async () => {
const contractIds = [
"CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP",
"CBGTG7XFRY3L6OKAUTR6KGDKUXUQBX3YDJ3QFDYTGVMOM7VV4O7NCODG",
];
const server = await getDevServer();
const url = new URL(
`http://localhost:${
(server?.server?.address() as any).port
}/api/v1/account-balances/${pubKey}`
);
url.searchParams.append("network", "PUBLIC");
for (const id of contractIds) {
url.searchParams.append("contract_ids", id);
}
const response = await fetch(url.href);
const data = await response.json();

expect(response.status).toEqual(200);
expect(
data.balances[
"BLND:GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56"
].isMalicious
).toEqual(true);
expect(
data.balances[
"TST:CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP"
].isMalicious
).toEqual(false);
register.clear();
await server.close();
});
it("doesn't check scanned status on Testnet", async () => {
const contractIds = [
"CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP",
"CBGTG7XFRY3L6OKAUTR6KGDKUXUQBX3YDJ3QFDYTGVMOM7VV4O7NCODG",
];
const server = await getDevServer();
const url = new URL(
`http://localhost:${
(server?.server?.address() as any).port
}/api/v1/account-balances/${pubKey}`
);
url.searchParams.append("network", "TESTNET");
for (const id of contractIds) {
url.searchParams.append("contract_ids", id);
}
const response = await fetch(url.href);
const data = await response.json();

expect(response.status).toEqual(200);
expect(
data.balances[
"BLND:GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56"
].isMalicious
).toEqual(false);
register.clear();
await server.close();
});
it("defaults to not malicious on scan status error", async () => {
const contractIds = [
"CCWAMYJME4H5CKG7OLXGC2T4M6FL52XCZ3OQOAV6LL3GLA4RO4WH3ASP",
"CBGTG7XFRY3L6OKAUTR6KGDKUXUQBX3YDJ3QFDYTGVMOM7VV4O7NCODG",
"CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ",
];
const server = await getDevServer();
const url = new URL(
`http://localhost:${
(server?.server?.address() as any).port
}/api/v1/account-balances/${pubKey}`
);
url.searchParams.append("network", "PUBLIC");
for (const id of contractIds) {
url.searchParams.append("contract_ids", id);
}
const response = await fetch(url.href);
const data = await response.json();

expect(response.status).toEqual(200);
expect(
data.balances[
"TST:CDP3XWJ4ZN222LKYBMWIY3GYXZYX3KA6WVNDS6V7WKXSYWLAEMYW7DTZ"
].isMalicious
).toEqual(false);
register.clear();
await server.close();
});
});
});
16 changes: 16 additions & 0 deletions src/route/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as StellarSdk from "stellar-sdk";

import { MercuryClient } from "../service/mercury";
import { BlockAidService } from "../service/blockaid";
import { addScannedStatus } from "../service/blockaid/helpers/addScanResults";
import { ajv } from "./validators";
import {
isContractId,
Expand Down Expand Up @@ -317,6 +318,21 @@ export async function initApiServer(
useMercury,
);

try {
data.balances = await addScannedStatus(
data.balances,
blockAidService,
network,
logger
);
} catch (e) {
data.balances = data.balances.map((bal: {}) => ({
...bal,
isMalicious: false,
}));
logger.error(e);
}

reply.code(200).send(data);
} catch (error) {
reply.code(500).send(ERROR.SERVER_ERROR);
Expand Down
37 changes: 37 additions & 0 deletions src/service/blockaid/helpers/addScanResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Logger } from "pino";
import { BlockAidService } from "..";
import { NetworkNames } from "../../../helper/validate";

export const addScannedStatus = async (
balances: { [key: string]: {} },
blockaidService: BlockAidService,
network: NetworkNames,
logger: Logger
) => {
const scannedBalances = {} as { [key: string]: { isMalicious: boolean } };
const entries = Object.entries(balances);

for (let i = 0; i < entries.length; i++) {
const [key, balanceInfo] = entries[i];
let data;
if (key !== "native" && network === "PUBLIC") {
// we only scan non-native assets on the public network
try {
const splitKey = key.split(":");
const blockaidKey = `${splitKey[0]}-${splitKey[1]}`;
const res = await blockaidService.scanAsset(blockaidKey);

data = res.data;
} catch (e) {
logger.error(e);
}
}

scannedBalances[key] = {
...balanceInfo,
isMalicious: Boolean(Number(data?.malicious_score)) ?? false,
};
}

return scannedBalances;
};

0 comments on commit 26bd893

Please sign in to comment.