@@ -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,83 @@ 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+ if ( "merchant" in item ) {
466+ const movement = {
467+ id : item . id ,
468+ timestamp : item . timestamp ,
469+ amount : - item . usdAmount ,
470+ title : item . merchant . name ,
471+ } ;
472+ if ( - item . usdAmount > 0 ) return { ...movement , detail : `Refund – Card **** ${ item . lastFour } ` } ;
473+ if ( item . type === "panda" ? item . operations . some ( ( { mode } ) => mode > 0 ) : item . mode > 0 )
474+ return { ...movement , detail : `Credit purchase – Card **** ${ item . lastFour } ` } ;
475+ return { ...movement , detail : `Debit purchase – Card **** ${ item . lastFour } ` } ;
476+ }
477+ switch ( item . type ) {
478+ case "received" :
479+ return {
480+ id : item . id ,
481+ timestamp : item . timestamp ,
482+ amount : item . usdAmount ,
483+ title : "Funds added" ,
484+ detail : `${ item . amount } ${ item . currency } ` ,
485+ } ;
486+ case "repay" :
487+ return {
488+ id : item . id ,
489+ timestamp : item . timestamp ,
490+ amount : - item . usdAmount ,
491+ title : "Debt payment" ,
492+ detail : `${ item . amount } ${ item . currency } ` ,
493+ } ;
494+ case "sent" :
495+ return {
496+ id : item . id ,
497+ timestamp : item . timestamp ,
498+ amount : - item . usdAmount ,
499+ title : `Sent to ${ mask ( item . receiver ) } ` ,
500+ detail : `${ item . amount } ${ item . currency } ` ,
501+ } ;
502+ }
503+ } ) ,
504+ cards : [
505+ ...Map . groupBy (
506+ items . filter ( ( item ) => "merchant" in item ) ,
507+ ( { cardId } ) => cardId ,
508+ ) ,
509+ ] . map ( ( [ cardId , cardItems ] ) => ( {
510+ ...cardItems . reduce (
511+ ( summary , { lastFour, usdAmount } ) => ( { amount : summary . amount + usdAmount , lastFour } ) ,
512+ { amount : 0 , lastFour : "" } ,
513+ ) ,
514+ cardId,
515+ } ) ) ,
516+ period :
517+ maturityDate === undefined
518+ ? undefined
519+ : `${ maturityDate . toLocaleDateString ( "en-US" , { month : "long" , timeZone : "UTC" } ) } , ${ maturityDate . getUTCFullYear ( ) } ` ,
520+ } ) ,
521+ ) ,
522+ ) ,
523+ 200 ,
524+ { "content-type" : "application/pdf" } ,
525+ ) ;
526+ }
527+
436528 if ( maturity !== undefined && pdf ) {
437529 const purchasesByCard = Map . groupBy (
438530 response . flatMap ( ( item ) => {
0 commit comments