@@ -40,6 +40,7 @@ import { anvil } from "viem/chains";
4040import fixedRate from "@exactly/common/fixedRate" ;
4141import chain , {
4242 exaPluginAbi ,
43+ exaPluginAddress ,
4344 exaPreviewerAbi ,
4445 exaPreviewerAddress ,
4546 marketAbi ,
@@ -50,11 +51,12 @@ import chain, {
5051 upgradeableModularAccountAbi ,
5152} from "@exactly/common/generated/chain" ;
5253import { decodeWithdraw } from "@exactly/common/ProposalType" ;
53- import { Address , Hash , type Hex } from "@exactly/common/validation" ;
54- import { effectiveRate , WAD } from "@exactly/lib" ;
54+ import { Address , Hash , Hex } from "@exactly/common/validation" ;
55+ import { effectiveRate , MATURITY_INTERVAL , WAD } from "@exactly/lib" ;
5556
5657import database , { cards , credentials , transactions } from "../database" ;
5758import auth from "../middleware/auth" ;
59+ import AccountStatement from "../utils/AccountStatement" ;
5860import { collectors as cryptomateCollectors } from "../utils/cryptomate" ;
5961import { collectors as pandaCollectors } from "../utils/panda" ;
6062import publicClient from "../utils/publicClient" ;
@@ -82,6 +84,15 @@ export default new Hono().get(
8284 async ( c ) => {
8385 const { include, maturity } = c . req . valid ( "query" ) ;
8486 if ( maturity !== undefined && maturity > 864e10 ) return c . json ( { code : "invalid maturity" } , 400 ) ;
87+ const pdf =
88+ accepts ( c , {
89+ header : "Accept" ,
90+ supports : [ "application/json" , "application/pdf" ] ,
91+ default : "application/json" ,
92+ } ) === "application/pdf" ;
93+ const accountPdf = pdf && include === undefined ;
94+ if ( pdf && include !== undefined && maturity === undefined )
95+ return c . json ( { code : "maturity required for filtered pdf" } , 400 ) ;
8596 function ignore ( type : InferInput < typeof ActivityTypes > ) {
8697 return include && ( Array . isArray ( include ) ? ! include . includes ( type ) : include !== type ) ;
8798 }
@@ -94,7 +105,7 @@ export default new Hono().get(
94105 cards : {
95106 columns : { id : true , lastFour : true } ,
96107 with : { transactions : { columns : { hashes : true , payload : true } } } ,
97- limit : ignore ( "card" ) || maturity !== undefined ? 0 : undefined ,
108+ limit : ignore ( "card" ) || ( maturity !== undefined && ! accountPdf ) ? 0 : undefined ,
98109 } ,
99110 } ,
100111 } ) ;
@@ -119,6 +130,14 @@ export default new Hono().get(
119130 . then ( ( logs ) => new Set ( logs . map ( ( { args } ) => args . plugin . toLowerCase ( ) as Hex ) ) )
120131 : Promise . resolve ( forbid ( new Set < Hex > ( ) ) ) ,
121132 ] ) ;
133+ const debtManager =
134+ ( ! ignore ( "repay" ) || ! ignore ( "received" ) ) && plugins . has ( parse ( Hex , exaPluginAddress . toLowerCase ( ) ) )
135+ ? await publicClient . readContract ( {
136+ address : exaPluginAddress ,
137+ functionName : "DEBT_MANAGER" ,
138+ abi : exaPluginAbi ,
139+ } )
140+ : undefined ;
122141
123142 const market = ( address : Hex ) => {
124143 const found = markets . get ( address . toLowerCase ( ) as Hex ) ;
@@ -132,7 +151,7 @@ export default new Hono().get(
132151 abi : marketAbi ,
133152 eventName : "RepayAtMaturity" ,
134153 address : [ ...markets . keys ( ) ] ,
135- args : { caller : [ ...plugins ] , borrower : account } ,
154+ args : { caller : [ ...plugins , ... ( debtManager === undefined ? [ ] : [ debtManager ] ) ] , borrower : account } ,
136155 toBlock : "latest" ,
137156 fromBlock : 0n ,
138157 strict : true ,
@@ -170,7 +189,7 @@ export default new Hono().get(
170189 ? [ ]
171190 : repayPromise . then ( ( logs ) =>
172191 logs
173- . filter ( ( { args } ) => maturity === undefined || Number ( args . maturity ) === maturity )
192+ . filter ( ( { args } ) => accountPdf || maturity === undefined || Number ( args . maturity ) === maturity )
174193 . map ( ( log ) =>
175194 parse ( RepayActivity , {
176195 ...log ,
@@ -265,7 +284,7 @@ export default new Hono().get(
265284 ) ;
266285 const timestamps = new Map ( blocks . map ( ( { number : block , timestamp } ) => [ block , timestamp ] ) ) ;
267286 const purchases =
268- ! ignore ( "card" ) && borrows && maturity !== undefined
287+ ! ignore ( "card" ) && borrows && maturity !== undefined && ! accountPdf
269288 ? await ( ( ) => {
270289 const hashes = borrows
271290 . entries ( )
@@ -286,13 +305,6 @@ export default new Hono().get(
286305 } ) ( )
287306 : credential . cards ;
288307
289- const accept = accepts ( c , {
290- header : "Accept" ,
291- supports : maturity === undefined ? [ "application/json" ] : [ "application/json" , "application/pdf" ] ,
292- default : "application/json" ,
293- } ) ;
294- const pdf = accept === "application/pdf" ;
295-
296308 const response = [
297309 ...purchases . flatMap ( ( { id : cardId , lastFour, transactions : txs } ) =>
298310 txs . map ( ( { hashes, payload } ) => {
@@ -303,7 +315,9 @@ export default new Hono().get(
303315 const b = borrows ?. get ( h as Hash ) ;
304316 if ( ! b ) return null ;
305317 const filtered =
306- maturity === undefined ? b . events : b . events . filter ( ( { maturity : m } ) => Number ( m ) === maturity ) ;
318+ maturity === undefined || accountPdf
319+ ? b . events
320+ : b . events . filter ( ( { maturity : m } ) => Number ( m ) === maturity ) ;
307321 if ( filtered . length === 0 ) return null ;
308322 return {
309323 events : maturity !== undefined && b . events . length > 1 ? b . events : filtered ,
@@ -363,11 +377,12 @@ export default new Hono().get(
363377 const hash = hashes [ 0 ] ;
364378 const borrow = borrows ?. get ( hash as Hash ) ;
365379 const filtered =
366- maturity === undefined || ! borrow
380+ maturity === undefined || accountPdf || ! borrow
367381 ? borrow ?. events
368382 : borrow . events . filter ( ( { maturity : m } ) => Number ( m ) === maturity ) ;
369383 if ( maturity !== undefined && borrow && filtered ?. length === 0 ) return ;
370- const events = ! borrow || maturity === undefined || borrow . events . length <= 1 ? filtered : borrow . events ;
384+ const events =
385+ ! borrow || maturity === undefined || accountPdf || borrow . events . length <= 1 ? filtered : borrow . events ;
371386 const cryptomate = safeParse (
372387 { 0 : DebitActivity , 1 : CreditActivity } [ events ?. length ?? 0 ] ?? InstallmentsActivity ,
373388 {
@@ -433,6 +448,76 @@ export default new Hono().get(
433448 . filter ( ( item ) => chain . id === anvil . id || ! ( "status" in item && item . status === "declined" ) )
434449 . toSorted ( ( a , b ) => b . timestamp . localeCompare ( a . timestamp ) || b . id . localeCompare ( a . id ) ) ;
435450
451+ if ( accountPdf ) {
452+ const maturityDate = maturity === undefined ? undefined : new Date ( maturity * 1000 ) ;
453+ const mask = ( address : Address ) => `${ address . slice ( 0 , 6 ) } ...${ address . slice ( - 6 ) } ` ;
454+ const items = response . filter (
455+ ( { timestamp } ) =>
456+ maturity === undefined ||
457+ ( Date . parse ( timestamp ) / 1000 >= maturity - MATURITY_INTERVAL && Date . parse ( timestamp ) / 1000 <= maturity ) ,
458+ ) ;
459+ return c . body (
460+ new Uint8Array (
461+ await renderToBuffer (
462+ AccountStatement ( {
463+ account : mask ( account ) ,
464+ activities : items . map ( ( item ) =>
465+ "merchant" in item
466+ ? {
467+ id : item . id ,
468+ timestamp : item . timestamp ,
469+ amount : - item . usdAmount ,
470+ title : item . merchant . name ,
471+ detail : `${ - item . usdAmount > 0 ? "Refund" : item . type === "panda" ? ( item . operations . some ( ( { mode } ) => mode > 0 ) ? "Credit purchase" : "Debit purchase" ) : item . mode > 0 ? "Credit purchase" : "Debit purchase" } – Card **** ${ item . lastFour } ` ,
472+ }
473+ : item . type === "received"
474+ ? {
475+ id : item . id ,
476+ timestamp : item . timestamp ,
477+ amount : item . usdAmount ,
478+ title : "Funds added" ,
479+ detail : `${ item . amount } ${ item . currency } ` ,
480+ }
481+ : item . type === "repay"
482+ ? {
483+ id : item . id ,
484+ timestamp : item . timestamp ,
485+ amount : - item . usdAmount ,
486+ title : "Debt payment" ,
487+ detail : `${ item . amount } ${ item . currency } ` ,
488+ }
489+ : {
490+ id : item . id ,
491+ timestamp : item . timestamp ,
492+ amount : - item . usdAmount ,
493+ title : `Sent to ${ mask ( item . receiver ) } ` ,
494+ detail : `${ item . amount } ${ item . currency } ` ,
495+ } ,
496+ ) ,
497+ cards : [
498+ ...Map . groupBy (
499+ items . filter ( ( item ) => "merchant" in item ) ,
500+ ( { cardId } ) => cardId ,
501+ ) ,
502+ ] . map ( ( [ cardId , cardItems ] ) => ( {
503+ ...cardItems . reduce (
504+ ( summary , { lastFour, usdAmount } ) => ( { amount : summary . amount + usdAmount , lastFour } ) ,
505+ { amount : 0 , lastFour : "" } ,
506+ ) ,
507+ cardId,
508+ } ) ) ,
509+ period :
510+ maturityDate === undefined
511+ ? undefined
512+ : `${ maturityDate . toLocaleDateString ( "en-US" , { month : "long" , timeZone : "UTC" } ) } , ${ maturityDate . getUTCFullYear ( ) } ` ,
513+ } ) ,
514+ ) ,
515+ ) ,
516+ 200 ,
517+ { "content-type" : "application/pdf" } ,
518+ ) ;
519+ }
520+
436521 if ( maturity !== undefined && pdf ) {
437522 const purchasesByCard = Map . groupBy (
438523 response . flatMap ( ( item ) => {
0 commit comments