@@ -3,18 +3,33 @@ import { Asset, DecodedTransaction } from '@3loop/transaction-decoder'
33
44export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'
55
6- export function filterZeroTransfers ( transfers : Asset [ ] ) : Asset [ ] {
7- return transfers . filter ( ( t ) => ( t . amount && t . amount !== '0' ) || ! t . amount )
8- }
6+ //------------------------------------------------------------------------------
7+ //Core helper functions
98
10- export function filterNullTransfers ( transfers : Asset [ ] ) : Asset [ ] {
11- return transfers . filter ( ( t ) => t . from !== NULL_ADDRESS && t . to !== NULL_ADDRESS )
9+ interface FilterOptions {
10+ excludeZero ?: boolean
11+ excludeNull ?: boolean
12+ excludeDuplicates ?: boolean
1213}
1314
14- export function filterDuplicateTransfers ( transfers : Asset [ ] ) : Asset [ ] {
15- return transfers . filter (
16- ( t , i , self ) => self . findIndex ( ( t2 ) => t2 . address === t . address && t2 . amount === t . amount ) === i ,
17- )
15+ export const filterTransfers = ( transfers : Asset [ ] , filters : FilterOptions = { } ) : Asset [ ] => {
16+ let filtered = [ ...transfers ]
17+
18+ if ( filters . excludeZero ) {
19+ filtered = filtered . filter ( ( t ) => ( t . amount && t . amount !== '0' ) || ! t . amount )
20+ }
21+
22+ if ( filters . excludeNull ) {
23+ filtered = filtered . filter ( ( t ) => t . from !== NULL_ADDRESS && t . to !== NULL_ADDRESS )
24+ }
25+
26+ if ( filters . excludeDuplicates ) {
27+ filtered = filtered . filter (
28+ ( t , i , self ) => self . findIndex ( ( t2 ) => t2 . address === t . address && t2 . amount === t . amount ) === i ,
29+ )
30+ }
31+
32+ return filtered
1833}
1934
2035export function toAssetTransfer ( transfer : Asset ) : AssetTransfer {
@@ -33,81 +48,21 @@ export function toAssetTransfer(transfer: Asset): AssetTransfer {
3348}
3449
3550export function assetsSent ( transfers : Asset [ ] , address : string ) : AssetTransfer [ ] {
36- let filteredTransfers = filterZeroTransfers ( transfers )
37-
38- if ( address !== NULL_ADDRESS ) {
39- filteredTransfers = filterNullTransfers ( filteredTransfers )
40- }
51+ const filtered = filterTransfers ( transfers , {
52+ excludeZero : true ,
53+ excludeNull : address !== NULL_ADDRESS ,
54+ } )
4155
42- return filteredTransfers . filter ( ( t ) => t . from . toLowerCase ( ) === address . toLowerCase ( ) ) . map ( toAssetTransfer )
56+ return filtered . filter ( ( t ) => t . from . toLowerCase ( ) === address . toLowerCase ( ) ) . map ( toAssetTransfer )
4357}
4458
4559export function assetsReceived ( transfers : Asset [ ] , address : string ) : AssetTransfer [ ] {
46- let filteredTransfers = filterZeroTransfers ( transfers )
47-
48- if ( address !== NULL_ADDRESS ) {
49- filteredTransfers = filterNullTransfers ( filteredTransfers )
50- }
51-
52- return filteredTransfers . filter ( ( t ) => t . to . toLowerCase ( ) === address . toLowerCase ( ) ) . map ( toAssetTransfer )
53- }
54-
55- export function displayAddress ( address : string ) : string {
56- return address . slice ( 0 , 6 ) + '...' + address . slice ( - 4 )
57- }
58-
59- export function isSwap ( event : DecodedTransaction ) : boolean {
60- if ( event . transfers . some ( ( t ) => t . type !== 'ERC20' && t . type !== 'native' ) ) return false
61-
62- const transfers = event . transfers . filter ( ( t ) => t . from !== NULL_ADDRESS && t . to !== NULL_ADDRESS )
63-
64- const sent = new Set (
65- transfers . filter ( ( t ) => t . from . toLowerCase ( ) === event . fromAddress . toLowerCase ( ) ) . map ( ( t ) => t . address ) ,
66- )
67- const received = new Set (
68- transfers . filter ( ( t ) => t . to . toLowerCase ( ) === event . fromAddress . toLowerCase ( ) ) . map ( ( t ) => t . address ) ,
69- )
70-
71- if ( sent . size !== 1 || received . size !== 1 || sent . values ( ) === received . values ( ) ) return false
72-
73- return true
74- }
75-
76- export function formatNumber ( numberString : string , precision ?: number ) : string {
77- const [ integerPart , decimalPart ] = numberString . split ( '.' )
78- const bigIntPart = BigInt ( integerPart )
79-
80- if ( ( integerPart && integerPart . length < 3 && ! decimalPart ) || ( decimalPart && decimalPart . startsWith ( '000' ) ) )
81- return numberString
82-
83- // Format the integer part manually
84- let formattedIntegerPart = ''
85- const integerStr = bigIntPart . toString ( )
86- for ( let i = 0 ; i < integerStr . length ; i ++ ) {
87- if ( i > 0 && ( integerStr . length - i ) % 3 === 0 ) {
88- formattedIntegerPart += ','
89- }
90- formattedIntegerPart += integerStr [ i ]
91- }
92-
93- // Format the decimal part
94- const formattedDecimalPart = decimalPart
95- ? parseFloat ( '0.' + decimalPart )
96- . toFixed ( precision ?? 3 )
97- . split ( '.' ) [ 1 ]
98- : '00'
99-
100- return formattedIntegerPart + '.' + formattedDecimalPart
101- }
102-
103- export function displayAsset ( asset : Payment | undefined ) : string {
104- if ( ! asset || ! asset . asset ) return 'unknown asset'
105-
106- const symbol = asset . asset . type === 'ERC20' ? asset . asset . symbol : asset . asset . name
107-
108- if ( symbol ) return formatNumber ( asset . amount ) + ' ' + symbol
60+ const filtered = filterTransfers ( transfers , {
61+ excludeZero : true ,
62+ excludeNull : address !== NULL_ADDRESS ,
63+ } )
10964
110- return formatNumber ( asset . amount ) + ' ' + displayAddress ( asset . asset . address )
65+ return filtered . filter ( ( t ) => t . to . toLowerCase ( ) === address . toLowerCase ( ) ) . map ( toAssetTransfer )
11166}
11267
11368export function getPayments ( {
@@ -203,27 +158,91 @@ export function processNftTransfers(transfers: Asset[]) {
203158 }
204159}
205160
206- export function displayPayments ( erc20Payments : Payment [ ] , nativePayments : Payment [ ] ) {
207- if ( erc20Payments . length > 0 && nativePayments . length > 0 ) {
208- const amount = ( erc20Payments . length + 1 ) . toString ( )
209- return amount + ' assets'
210- } else if ( erc20Payments . length > 0 ) {
211- return (
212- erc20Payments [ 0 ] . amount +
213- ' ' +
214- ( erc20Payments [ 0 ] . asset ?. symbol || erc20Payments [ 0 ] . asset ?. name + ' tokens' || 'ERCC20 tokens' )
215- )
216- } else if ( nativePayments . length > 0 ) {
217- return (
218- nativePayments [ 0 ] . amount +
219- ' ' +
220- ( nativePayments [ 0 ] . asset ?. symbol || nativePayments [ 0 ] . asset ?. name + ' tokens' || 'native tokens' )
221- )
161+ //------------------------------------------------------------------------------
162+ // Formatting Functions
163+
164+ export function displayAddress ( address : string ) : string {
165+ return address . slice ( 0 , 6 ) + '...' + address . slice ( - 4 )
166+ }
167+
168+ export const formatNumber = ( numberString : string , precision = 3 ) : string => {
169+ const [ integerPart , decimalPart ] = numberString . split ( '.' )
170+ const bigIntPart = BigInt ( integerPart )
171+
172+ if ( ( integerPart && integerPart . length < 3 && ! decimalPart ) || ( decimalPart && decimalPart . startsWith ( '000' ) ) )
173+ return numberString
174+
175+ // Format the integer part manually
176+ let formattedIntegerPart = ''
177+ const integerStr = bigIntPart . toString ( )
178+ for ( let i = 0 ; i < integerStr . length ; i ++ ) {
179+ if ( i > 0 && ( integerStr . length - i ) % 3 === 0 ) {
180+ formattedIntegerPart += ','
181+ }
182+ formattedIntegerPart += integerStr [ i ]
183+ }
184+
185+ // Format the decimal part
186+ const formattedDecimalPart = decimalPart
187+ ? parseFloat ( '0.' + decimalPart )
188+ . toFixed ( precision )
189+ . split ( '.' ) [ 1 ]
190+ : '00'
191+
192+ return formattedIntegerPart + '.' + formattedDecimalPart
193+ }
194+
195+ export const displayAsset = ( asset ?: Payment ) : string => {
196+ if ( ! asset ?. asset ) return 'unknown asset'
197+
198+ const symbol = asset . asset . type === 'ERC20' ? asset . asset . symbol : asset . asset . name
199+
200+ if ( symbol ) return formatNumber ( asset . amount ) + ' ' + symbol
201+
202+ return formatNumber ( asset . amount ) + ' ' + displayAddress ( asset . asset . address )
203+ }
204+
205+ export function displayAssets ( assets : Payment [ ] ) {
206+ const erc20 = assets . filter ( ( a ) => a . asset . type === 'ERC20' )
207+ const native = assets . filter ( ( a ) => a . asset . type === 'native' )
208+
209+ if ( assets . length === 1 ) {
210+ return displayAsset ( assets [ 0 ] )
211+ } else if ( erc20 . length > 0 && native . length > 0 ) {
212+ return ( erc20 . length + 1 ) . toString ( ) + ' assets'
213+ } else if ( erc20 . length > 0 ) {
214+ return erc20 [ 0 ] . amount + ' ' + ( erc20 [ 0 ] . asset ?. symbol || erc20 [ 0 ] . asset ?. name + ' tokens' || 'ERCC20 tokens' )
215+ } else if ( native . length > 0 ) {
216+ return native [ 0 ] . amount + ' ' + ( native [ 0 ] . asset ?. symbol || native [ 0 ] . asset ?. name + ' tokens' || 'native tokens' )
222217 } else {
223218 return ''
224219 }
225220}
226221
222+ //------------------------------------------------------------------------------
223+ // Categorization Functions
224+
225+ export function isSwap ( event : DecodedTransaction ) : boolean {
226+ if ( event . transfers . some ( ( t ) => t . type !== 'ERC20' && t . type !== 'native' ) ) return false
227+
228+ const transfers = filterTransfers ( event . transfers , {
229+ excludeZero : true ,
230+ excludeNull : true ,
231+ } )
232+
233+ const uniqueSent = new Set (
234+ transfers . filter ( ( t ) => t . from . toLowerCase ( ) === event . fromAddress . toLowerCase ( ) ) . map ( ( t ) => t . address ) ,
235+ )
236+ const uniqueReceived = new Set (
237+ transfers . filter ( ( t ) => t . to . toLowerCase ( ) === event . fromAddress . toLowerCase ( ) ) . map ( ( t ) => t . address ) ,
238+ )
239+
240+ if ( uniqueSent . size !== 1 || uniqueReceived . size !== 1 || uniqueSent . values ( ) === uniqueReceived . values ( ) )
241+ return false
242+
243+ return true
244+ }
245+
227246export function defaultEvent ( event : DecodedTransaction ) : InterpretedTransaction {
228247 const burned = assetsReceived ( event . transfers , NULL_ADDRESS )
229248 const minted = assetsSent ( event . transfers , NULL_ADDRESS )
@@ -246,13 +265,17 @@ export function defaultEvent(event: DecodedTransaction): InterpretedTransaction
246265
247266export function categorizedDefaultEvent ( event : DecodedTransaction ) : InterpretedTransaction {
248267 const newEvent = defaultEvent ( event )
249- const nonDuplicateTransfers = filterDuplicateTransfers ( event . transfers )
250- const nonZeroTransfers = filterZeroTransfers ( nonDuplicateTransfers )
268+ const transfers = filterTransfers ( event . transfers , {
269+ excludeDuplicates : true ,
270+ } )
271+ const nonZeroTransfers = filterTransfers ( transfers , {
272+ excludeZero : true ,
273+ } )
251274 const minted = newEvent . assetsMinted || [ ]
252275 const burned = newEvent . assetsBurned || [ ]
253276
254277 // single burn
255- if ( burned . length === 1 && nonDuplicateTransfers . length <= 2 ) {
278+ if ( burned . length === 1 && nonZeroTransfers . length <= 2 ) {
256279 return {
257280 ...newEvent ,
258281 type : 'burn' ,
@@ -261,7 +284,7 @@ export function categorizedDefaultEvent(event: DecodedTransaction): InterpretedT
261284 }
262285
263286 // single mint
264- if ( minted . length === 1 && nonDuplicateTransfers . length <= 2 ) {
287+ if ( minted . length === 1 && transfers . length <= 2 ) {
265288 const price = newEvent . assetsSent . length === 1 ? newEvent . assetsSent [ 0 ] : undefined
266289 return {
267290 ...newEvent ,
@@ -308,5 +331,23 @@ export function categorizedDefaultEvent(event: DecodedTransaction): InterpretedT
308331 }
309332 }
310333
334+ if ( isSwap ( event ) ) {
335+ const netSent = getPayments ( {
336+ transfers : event . transfers ,
337+ fromAddresses : [ event . fromAddress ] ,
338+ } )
339+
340+ const netReceived = getPayments ( {
341+ transfers : event . transfers ,
342+ toAddresses : [ event . fromAddress ] ,
343+ } )
344+
345+ return {
346+ ...newEvent ,
347+ type : 'swap' ,
348+ action : 'Swapped ' + displayAsset ( netSent [ 0 ] ) + ' for ' + displayAsset ( netReceived [ 0 ] ) ,
349+ }
350+ }
351+
311352 return newEvent
312353}
0 commit comments