Skip to content

Commit

Permalink
Merge pull request #23 from Once-Upon/benguyen0214/ou-1150-erc1155-pu…
Browse files Browse the repository at this point in the history
…rchase

Update NFT purchase/sale contextualizers
  • Loading branch information
pcowgill committed Nov 30, 2023
2 parents 46f718a + 116fa9a commit 3c29216
Show file tree
Hide file tree
Showing 13 changed files with 1,041 additions and 243 deletions.
4 changes: 2 additions & 2 deletions src/heuristics/erc1155Purchase.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Transaction } from '../types';
import { detectERC1155Purchase } from './erc1155Purchase';
import erc1155Purchase0x16b2334d from '../test/transactions/erc1155Purchase-0x16b2334d.json';
import erc1155Purchase0x156df9f7 from '../test/transactions/erc1155Purchase-0x156df9f7.json';

describe('ERC1155 Purchase', () => {
it('Should detect ERC1155 Purchase transaction', () => {
const isERC1155Purchase1 = detectERC1155Purchase(
erc1155Purchase0x16b2334d as Transaction,
erc1155Purchase0x156df9f7 as Transaction,
);
expect(isERC1155Purchase1).toBe(true);
});
Expand Down
12 changes: 12 additions & 0 deletions src/heuristics/erc1155Sale.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Transaction } from '../types';
import { detectERC1155Sale } from './erc1155Sale';
import erc1155Sale0x16b2334d from '../test/transactions/erc1155Sale-0x16b2334d.json';

describe('ERC1155 Sale', () => {
it('Should detect ERC1155 Sale transaction', () => {
const isERC1155Sale1 = detectERC1155Sale(
erc1155Sale0x16b2334d as Transaction,
);
expect(isERC1155Sale1).toBe(true);
});
});
148 changes: 148 additions & 0 deletions src/heuristics/erc1155Sale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { ethers } from 'ethers';
import { Asset, Transaction } from '../types';

export function erc1155SaleContextualizer(
transaction: Transaction,
): Transaction {
const isERC1155Sale = detectERC1155Sale(transaction);
if (!isERC1155Sale) return transaction;

return generateERC1155SaleContext(transaction);
}

/**
* Detection criteria
*
* A tx is an ERC1155 sale when the tx.from sends and receives exactly 1 asset (look at netAssetTransfers).
* The tx.from must send exactly 1 ERC1155, where the value (special to 1155s) can be arbitrary
* The tx.from must receive either ETH/WETH/Blur ETH
* There are no other recipients of ERC721/ERC20s/ERC1155s.
*/
export function detectERC1155Sale(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)
: [];
// check if transfer.from sent and received one asset
const transfers = transaction.netAssetTransfers[transaction.from];
const nftsSent = transfers.sent.filter((t) => t.type === 'erc1155');
const tokenReceived = transfers.received.filter(
(t) => t.type === 'eth' || t.type === 'erc20',
);

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

return false;
}

function generateERC1155SaleContext(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;
}
100 changes: 0 additions & 100 deletions src/heuristics/erc1155Swap.ts

This file was deleted.

18 changes: 0 additions & 18 deletions src/heuristics/erc721Purchase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,12 @@ import {
generateERC21PurchaseContext,
} from './erc721Purchase';
import erc721Purchase0x2558f104 from '../test/transactions/erc721Purchase-0x2558f104.json';
import erc721Purchase0x05b8cee6 from '../test/transactions/erc721Purchase-0x05b8cee6.json';
import erc721Purchase0xdeba4248 from '../test/transactions/erc721Purchase-0xdeba4248.json';

describe('ERC721 Purchase', () => {
it('Should detect ERC721 Purchase transaction', () => {
const isERC721Purchase1 = detectERC721Purchase(
erc721Purchase0x2558f104 as Transaction,
);
expect(isERC721Purchase1).toBe(true);

const isERC721Purchase2 = detectERC721Purchase(
erc721Purchase0x05b8cee6 as Transaction,
);
expect(isERC721Purchase2).toBe(true);
const isERC721Purchase3 = detectERC721Purchase(
erc721Purchase0xdeba4248 as Transaction,
);
expect(isERC721Purchase3).toBe(true);
});

it('Should create ERC721 Purchase context', () => {
const result1 = generateERC21PurchaseContext(
erc721Purchase0xdeba4248 as Transaction,
);
expect(result1.context.summaries.en.title).toBe('NFT Purchase');
});
});
37 changes: 15 additions & 22 deletions src/heuristics/erc721Purchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export function erc721PurchaseContextualizer(
return generateERC21PurchaseContext(transaction);
}

/**
* Detection criteria
*
* Transaction.from sends ETH/WETH/blur eth and receives only NFTs.
* Nothing is minted (exception being weth)
* In netAssetTransfers, the address that sent the NFTs receives either eth/weth/blur eth.
* The rest of the parties only receive eth/weth/blur eth (royalties/fees)
*/
export function detectERC721Purchase(transaction: Transaction): boolean {
/**
* There is a degree of overlap between the 'detect' and 'generateContext' functions,
Expand All @@ -20,29 +28,14 @@ export function detectERC721Purchase(transaction: Transaction): boolean {
*/
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 === 'erc721');
const nftsSent = transfers.sent.filter((t) => t.type === 'erc721');

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;
}
const transfers = transaction.netAssetTransfers[transaction.from];
const nftsReceived = transfers.received.filter((t) => t.type === 'erc721');
const tokenSent = transfers.sent.filter(
(t) => t.type === 'eth' || t.type === 'erc20',
);

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

return false;
Expand Down
10 changes: 10 additions & 0 deletions src/heuristics/erc721Sale.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Transaction } from '../types';
import { detectERC721Sale } from './erc721Sale';
import erc721Sale0x05b8cee6 from '../test/transactions/erc721Sale-0x05b8cee6.json';

describe('ERC721 Sale', () => {
it('Should detect ERC721 Sale transaction', () => {
const isERC721Sale1 = detectERC721Sale(erc721Sale0x05b8cee6 as Transaction);
expect(isERC721Sale1).toBe(true);
});
});
Loading

0 comments on commit 3c29216

Please sign in to comment.