@@ -3,6 +3,7 @@ package async_geyser
3
3
import (
4
4
"context"
5
5
"fmt"
6
+ "math"
6
7
"strconv"
7
8
"strings"
8
9
"time"
@@ -11,6 +12,7 @@ import (
11
12
"github.com/pkg/errors"
12
13
13
14
commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1"
15
+ transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
14
16
15
17
"github.com/code-payments/code-server/pkg/cache"
16
18
chat_util "github.com/code-payments/code-server/pkg/code/chat"
@@ -21,8 +23,10 @@ import (
21
23
"github.com/code-payments/code-server/pkg/code/data/deposit"
22
24
"github.com/code-payments/code-server/pkg/code/data/fulfillment"
23
25
"github.com/code-payments/code-server/pkg/code/data/intent"
26
+ "github.com/code-payments/code-server/pkg/code/data/onramp"
24
27
"github.com/code-payments/code-server/pkg/code/data/transaction"
25
28
"github.com/code-payments/code-server/pkg/code/push"
29
+ "github.com/code-payments/code-server/pkg/code/thirdparty"
26
30
currency_lib "github.com/code-payments/code-server/pkg/currency"
27
31
"github.com/code-payments/code-server/pkg/database/query"
28
32
"github.com/code-payments/code-server/pkg/kin"
@@ -252,7 +256,19 @@ func processPotentialExternalDeposit(ctx context.Context, conf *conf, data code_
252
256
}
253
257
254
258
if isCodeSwap {
255
- chatMessage , err := chat_util .ToKinAvailableForUseMessage (signature , usdcQuarksSwapped , blockTime )
259
+ purchases , err := getPurchasesFromSwap (
260
+ ctx ,
261
+ conf ,
262
+ data ,
263
+ signature ,
264
+ usdcSwapAccount ,
265
+ usdcQuarksSwapped ,
266
+ )
267
+ if err != nil {
268
+ return errors .Wrap (err , "error getting swap purchases" )
269
+ }
270
+
271
+ chatMessage , err := chat_util .ToKinAvailableForUseMessage (signature , blockTime , purchases ... )
256
272
if err != nil {
257
273
return errors .Wrap (err , "error creating chat message" )
258
274
}
@@ -452,6 +468,185 @@ func getCodeSwapMetadata(ctx context.Context, conf *conf, tokenBalances *solana.
452
468
return true , usdcAccount , usdcPaid , nil
453
469
}
454
470
471
+ func getPurchasesFromSwap (
472
+ ctx context.Context ,
473
+ conf * conf ,
474
+ data code_data.Provider ,
475
+ signature string ,
476
+ usdcSwapAccount * common.Account ,
477
+ usdcQuarksSwapped uint64 ,
478
+ ) ([]* transactionpb.ExchangeDataWithoutRate , error ) {
479
+ accountInfoRecord , err := data .GetAccountInfoByTokenAddress (ctx , usdcSwapAccount .PublicKey ().ToBase58 ())
480
+ if err != nil {
481
+ return nil , errors .Wrap (err , "error getting account info record" )
482
+ } else if accountInfoRecord .AccountType != commonpb .AccountType_SWAP {
483
+ return nil , errors .New ("usdc account is not a code swap account" )
484
+ }
485
+
486
+ cursorValue , err := base58 .Decode (signature )
487
+ if err != nil {
488
+ return nil , err
489
+ }
490
+
491
+ pageSize := 32
492
+ history , err := data .GetBlockchainHistory (
493
+ ctx ,
494
+ usdcSwapAccount .PublicKey ().ToBase58 (),
495
+ solana .CommitmentFinalized ,
496
+ query .WithCursor (cursorValue ),
497
+ query .WithLimit (uint64 (pageSize )),
498
+ )
499
+ if err != nil {
500
+ return nil , errors .Wrap (err , "error getting transaction history" )
501
+ }
502
+
503
+ purchases , err := func () ([]* transactionpb.ExchangeDataWithoutRate , error ) {
504
+ var res []* transactionpb.ExchangeDataWithoutRate
505
+ var usdcDeposited uint64
506
+ for _ , historyItem := range history {
507
+ if historyItem .Err != nil {
508
+ continue
509
+ }
510
+
511
+ tokenBalances , err := data .GetBlockchainTransactionTokenBalances (ctx , base58 .Encode (historyItem .Signature [:]))
512
+ if err != nil {
513
+ return nil , errors .Wrap (err , "error getting token balances" )
514
+ }
515
+ blockTime := time .Now ()
516
+ if historyItem .BlockTime != nil {
517
+ blockTime = * historyItem .BlockTime
518
+ }
519
+
520
+ isCodeSwap , _ , _ , err := getCodeSwapMetadata (ctx , conf , tokenBalances )
521
+ if err != nil {
522
+ return nil , errors .Wrap (err , "error getting code swap metadata" )
523
+ }
524
+
525
+ // Found another swap, so stop searching for purchases
526
+ if isCodeSwap {
527
+ // The amount of USDC deposited doesn't equate to the amount we
528
+ // swapped. There's either a race condition between a swap and
529
+ // deposit, or the user is manually moving funds in the account.
530
+ //
531
+ // Either way, the current algorithm can't properly assign pruchases
532
+ // to the swap, so return an empty result.
533
+ if usdcDeposited != usdcQuarksSwapped {
534
+ return nil , nil
535
+ }
536
+
537
+ return res , nil
538
+ }
539
+
540
+ deltaQuarks , err := getDeltaQuarksFromTokenBalances (usdcSwapAccount , tokenBalances )
541
+ if err != nil {
542
+ return nil , errors .Wrap (err , "error getting delta usdc from token balances" )
543
+ }
544
+
545
+ // Skip any USDC withdrawals. The average user will not be able to
546
+ // do this anyways since the swap account is derived off the 12 words.
547
+ if deltaQuarks <= 0 {
548
+ continue
549
+ }
550
+
551
+ usdAmount := float64 (deltaQuarks ) / float64 (usdc .QuarksPerUsdc )
552
+ usdcDeposited += uint64 (deltaQuarks )
553
+
554
+ // Disregard any USDC deposits for inconsequential amounts to avoid
555
+ // spam coming through
556
+ if usdAmount < 0.01 {
557
+ continue
558
+ }
559
+
560
+ rawUsdPurchase := & transactionpb.ExchangeDataWithoutRate {
561
+ Currency : "usd" ,
562
+ NativeAmount : usdAmount ,
563
+ }
564
+
565
+ // There is no memo for a blockchain message
566
+ if historyItem .Memo == nil {
567
+ res = append ([]* transactionpb.ExchangeDataWithoutRate {rawUsdPurchase }, res ... )
568
+ continue
569
+ }
570
+
571
+ // Attempt to parse a blockchain message from the memo, which will contain a
572
+ // nonce that maps to a fiat purchase from an onramp.
573
+ memoParts := strings .Split (* historyItem .Memo , " " )
574
+ memoMessage := memoParts [len (memoParts )- 1 ]
575
+ blockchainMessage , err := thirdparty .DecodeFiatOnrampPurchaseMessage ([]byte (memoMessage ))
576
+ if err != nil {
577
+ res = append ([]* transactionpb.ExchangeDataWithoutRate {rawUsdPurchase }, res ... )
578
+ continue
579
+ }
580
+ onrampRecord , err := data .GetFiatOnrampPurchase (ctx , blockchainMessage .Nonce )
581
+ if err == onramp .ErrPurchaseNotFound {
582
+ res = append ([]* transactionpb.ExchangeDataWithoutRate {rawUsdPurchase }, res ... )
583
+ continue
584
+ } else if err != nil {
585
+ return nil , errors .Wrap (err , "error getting onramp record" )
586
+ }
587
+
588
+ // This nonce is not associated with the owner account linked to the
589
+ // fiat purchase.
590
+ if onrampRecord .Owner != accountInfoRecord .OwnerAccount {
591
+ res = append ([]* transactionpb.ExchangeDataWithoutRate {rawUsdPurchase }, res ... )
592
+ continue
593
+ }
594
+
595
+ // Ensure the amounts make some sense wrt FX rates if we have them. We
596
+ // allow a generous buffer of 10% to account for fees that might be
597
+ // taken off of the deposited amount.
598
+ var usdRate , otherCurrencyRate float64
599
+ usdRateRecord , err := data .GetExchangeRate (ctx , currency_lib .USD , blockTime )
600
+ if err == nil {
601
+ usdRate = usdRateRecord .Rate
602
+ }
603
+ otherCurrencyRateRecord , err := data .GetExchangeRate (ctx , currency_lib .Code (onrampRecord .Currency ), blockTime )
604
+ if err == nil {
605
+ otherCurrencyRate = otherCurrencyRateRecord .Rate
606
+ }
607
+ if usdRate != 0 && otherCurrencyRate != 0 {
608
+ fxRate := otherCurrencyRate / usdRate
609
+ pctDiff := math .Abs (usdAmount * fxRate - onrampRecord .Amount ) / onrampRecord .Amount
610
+ if pctDiff > 0.1 {
611
+ res = append ([]* transactionpb.ExchangeDataWithoutRate {rawUsdPurchase }, res ... )
612
+ continue
613
+ }
614
+ }
615
+
616
+ res = append ([]* transactionpb.ExchangeDataWithoutRate {
617
+ {
618
+ Currency : onrampRecord .Currency ,
619
+ NativeAmount : onrampRecord .Amount ,
620
+ },
621
+ }, res ... )
622
+ }
623
+
624
+ if len (history ) < pageSize {
625
+ // At the end of history, so return the result
626
+ return res , nil
627
+ }
628
+ // Didn't find another swap, so we didn't find the full purchase history.
629
+ // Return an empty result.
630
+ //
631
+ // todo: Continue looking back into history
632
+ return nil , nil
633
+ }()
634
+ if err != nil {
635
+ return nil , err
636
+ }
637
+
638
+ if len (purchases ) == 0 {
639
+ // No purchases were returned, so defer back to the USDC amount swapped
640
+ return []* transactionpb.ExchangeDataWithoutRate {
641
+ {
642
+ Currency : "usd" ,
643
+ NativeAmount : float64 (usdcQuarksSwapped ) / float64 (usdc .QuarksPerUsdc ),
644
+ },
645
+ }, nil
646
+ }
647
+ return purchases , nil
648
+ }
649
+
455
650
// Optimistically tries to cache a balance for an external account not managed
456
651
// Code. It doesn't need to be perfect and will be lazily corrected on the next
457
652
// balance fetch with a newer state returned by a RPC node.
0 commit comments