Skip to content

Commit

Permalink
Merge pull request #6 from Once-Upon/benguyen0214/ou-1070-port-heuris…
Browse files Browse the repository at this point in the history
…tic-and-protocol-contextualizers-not-bridge-ones

Add heuristics and protocol contextualizers
  • Loading branch information
pcowgill authored Nov 29, 2023
2 parents c1afcdf + e3b8847 commit 04a1b2e
Show file tree
Hide file tree
Showing 63 changed files with 53,426 additions and 107 deletions.
830 changes: 824 additions & 6 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"dependencies": {
"ethers": "^5.6.4"
}
}
27 changes: 27 additions & 0 deletions src/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const TOKEN_SWAP_CONTRACTS = [
'0xe592427a0aece92de3edee1f18e0157c05861564', // Uniswap V3 Router
'0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45', // Uniswap V3router2
'0x7a250d5630b4cf539739df2c5dacb4c659f2488d', // Uniswap V2 Router2
'0xef1c6e67703c7bd7107eed8303fbe6ec2554bf6b', // Uniswap Universal Router1
'0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', // Uniswap Universal Router2
'0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f', // Sushiswap Router
'0x1111111254fb6c44bac0bed2854e76f90643097d', // 1inch Router
'0x881d40237659c251811cec9c364ef91dc08d300c', // Metamask Swap Router
'0xe66b31678d6c16e9ebf358268a790b763c133750', // Coinbase Wallet Swapper
'0x00000000009726632680fb29d3f7a9734e3010e2', // Rainbow Router
'0xdef1c0ded9bec7f1a1670819833240f027b25eff', // 0x Exchange Proxy. NOTE - This is both an erc20 swap and erc721 swap contract. This address is in both contract lists.
];

export const AIRDROP_THRESHOLD = 10;

export const KNOWN_ADDRESSES = {
NULL: '0x0000000000000000000000000000000000000000',
WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
};

export const WETH_ADDRESSES = [
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // Ethereum
'0x4200000000000000000000000000000000000006', // Optimism
'0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f', // Linea
'0x0000000000a39bb272e79075ade125fd351887ac', // Blur
];
17 changes: 17 additions & 0 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const VALID_CHARS =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890.? ';

export const hexToString = (str: string) => {
const buf = Buffer.from(str, 'hex');
return buf.toString('utf8');
};

export const countValidChars = (stringToCount: string) => {
let count = 0;
for (let i = 0; i < stringToCount.length; i++) {
if (VALID_CHARS.indexOf(stringToCount[i]) >= 0) {
count++;
}
}
return count;
};
43 changes: 43 additions & 0 deletions src/heuristics/cancelPendingTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Transaction } from '../types';

export function cancelPendingTransactionContextualizer(
transaction: Transaction,
): Transaction {
const isCanceledPendingTransaction =
detectCancelPendingTransaction(transaction);

if (!isCanceledPendingTransaction) return transaction;

return generateCancelPendingTransactionContext(transaction);
}

export function detectCancelPendingTransaction(
transaction: Transaction,
): boolean {
// Check if user cancelled pending transaction
if (
transaction.to === transaction.from &&
(transaction.input === '0x' || transaction.input === '0x0') &&
transaction.value === '0'
) {
return true;
}

return false;
}

export function generateCancelPendingTransactionContext(
transaction: Transaction,
): Transaction {
transaction.context = {
summaries: {
category: 'DEV',
en: {
title: 'cancelPendingTransaction',
default: '',
},
},
};

return transaction;
}
12 changes: 12 additions & 0 deletions src/heuristics/contractDeployment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Transaction } from '../types';
import { detectContractDeployment } from './contractDeployment';
import contractDeployed0x88e7d866 from '../test/transactions/contractDeployed-0x88e7d866.json';

describe('Contract Deployed', () => {
it('Should detect contract deployment transaction', () => {
const contractDeployed1 = detectContractDeployment(
contractDeployed0x88e7d866 as Transaction,
);
expect(contractDeployed1).toBe(true);
});
});
50 changes: 50 additions & 0 deletions src/heuristics/contractDeployment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Transaction } from '../types';

export function contractDeploymentContextualizer(
transaction: Transaction,
): Transaction {
const isContractDeployment = detectContractDeployment(transaction);

if (!isContractDeployment) return transaction;

return generateContractDeploymentContext(transaction);
}

export function detectContractDeployment(transaction: Transaction): boolean {
if (transaction.to === null && transaction.receipt?.contractAddress) {
return true;
}
return false;
}

function generateContractDeploymentContext(
transaction: Transaction,
): Transaction {
transaction.context = {
variables: {
deployerAddress: {
type: 'address',
value: transaction.from,
},
contractAddress: {
type: 'address',
value: transaction.receipt?.contractAddress,
},
},
summaries: {
category: 'DEV',
en: {
title: 'Contract Deployed',
default: '[[deployerAddress]] [[deployed]] [[contractAddress]]',
variables: {
deployed: {
type: 'contextAction',
value: 'Deployed',
},
},
},
},
};

return transaction;
}
12 changes: 12 additions & 0 deletions src/heuristics/erc1155Purchase.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Transaction } from '../types';
import { detectERC1155Purchase } from './erc1155Purchase';
import erc1155Purchase0x16b2334d from '../test/transactions/erc1155Purchase-0x16b2334d.json';

describe('ERC1155 Purchase', () => {
it('Should detect ERC1155 Purchase transaction', () => {
const isERC1155Purchase1 = detectERC1155Purchase(
erc1155Purchase0x16b2334d as Transaction,
);
expect(isERC1155Purchase1).toBe(true);
});
});
151 changes: 151 additions & 0 deletions src/heuristics/erc1155Purchase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { ethers } from 'ethers';
import { Asset, Transaction } from '../types';

export function erc1155PurchaseContextualizer(
transaction: Transaction,
): Transaction {
const isERC1155Purchase = detectERC1155Purchase(transaction);
if (!isERC1155Purchase) return transaction;

return generateERC1155PurchaseContext(transaction);
}

export function detectERC1155Purchase(transaction: Transaction): boolean {
/**
* There is a degree of overlap between the 'detect' and 'generateContext' functions,
* and while this might seem redundant, maintaining the 'detect' function aligns with
* established patterns in our other modules. This consistency is beneficial,
* and it also serves to decouple the logic, thereby simplifying the testing process
*/

if (!transaction.netAssetTransfers) return false;

const addresses = transaction.netAssetTransfers
? Object.keys(transaction.netAssetTransfers)
: [];

for (const address of addresses) {
const transfers = transaction.netAssetTransfers[address];
const nftsReceived = transfers.received.filter((t) => t.type === 'erc1155');
const nftsSent = transfers.sent.filter((t) => t.type === 'erc1155');

const ethOrErc20Sent = transfers.sent.filter(
(t) => t.type === 'eth' || t.type === 'erc20',
);
const ethOrErc20Received = transfers.received.filter(
(t) => t.type === 'eth' || t.type === 'erc20',
);

if (nftsReceived.length > 0 && ethOrErc20Sent.length > 0) {
return true;
}

if (nftsSent.length > 0 && ethOrErc20Received.length > 0) {
return true;
}
}

return false;
}

function generateERC1155PurchaseContext(transaction: Transaction): Transaction {
const receivingAddresses: string[] = [];
const receivedNfts: Asset[] = [];
const sentPayments: { type: string; asset: string; value: string }[] = [];

for (const [address, data] of Object.entries(transaction.netAssetTransfers)) {
const nftTransfers = data.received.filter((t) => t.type === 'erc1155');
const paymentTransfers = data.sent.filter(
(t) => t.type === 'erc20' || t.type === 'eth',
);
if (nftTransfers.length > 0) {
receivingAddresses.push(address);
nftTransfers.forEach((nft) => receivedNfts.push(nft));
}
if (paymentTransfers.length > 0) {
paymentTransfers.forEach((payment) =>
sentPayments.push({
type: payment.type,
asset: payment.asset,
value: payment.value,
}),
);
}
}

const receivedNftContracts = Array.from(
new Set(receivedNfts.map((x) => x.asset)),
);
const totalPayments = Object.values(
sentPayments.reduce((acc, next) => {
acc[next.asset] = {
type: next.type,
asset: next.asset,
value: ethers.BigNumber.from(acc[next.asset]?.value || '0')
.add(next.value)
.toString(),
};
return acc;
}, {}),
) as { type: 'eth' | 'erc20'; asset: string; value: string }[];

transaction.context = {
variables: {
userOrUsers: {
type: receivingAddresses.length > 1 ? 'emphasis' : 'address',
value:
receivingAddresses.length > 1
? `${receivingAddresses.length} Users`
: receivingAddresses[0],
},
tokenOrTokens:
receivedNfts.length === 1
? {
type: 'erc1155',
token: receivedNfts[0].asset,
tokenId: receivedNfts[0].tokenId,
value: receivedNfts[0].value,
}
: receivedNftContracts.length === 1
? {
type: 'address',
value: receivedNftContracts[0],
}
: {
type: 'emphasis',
value: `${receivedNfts.length} NFTs`,
},
price:
totalPayments.length > 1
? {
type: 'emphasis',
value: `${totalPayments.length} Assets`,
}
: totalPayments[0].type === 'eth'
? {
type: 'eth',
value: totalPayments[0].value,
}
: {
type: 'erc20',
token: totalPayments[0].asset,
value: totalPayments[0].value,
},
},
summaries: {
category: 'NFT',
en: {
title: 'NFT Purchase',
default: '[[userOrUsers]] [[bought]] [[tokenOrTokens]] for [[price]]',
variables: {
bought: {
type: 'contextAction',
value: 'Bought',
},
},
},
},
};

return transaction;
}
Loading

0 comments on commit 04a1b2e

Please sign in to comment.