Skip to content

Commit 76d9c8f

Browse files
authored
Link swaps to fiat purchases from onramps (#69)
1 parent 538bf5c commit 76d9c8f

File tree

3 files changed

+208
-12
lines changed

3 files changed

+208
-12
lines changed

pkg/code/async/geyser/external_deposit.go

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package async_geyser
33
import (
44
"context"
55
"fmt"
6+
"math"
67
"strconv"
78
"strings"
89
"time"
@@ -11,6 +12,7 @@ import (
1112
"github.com/pkg/errors"
1213

1314
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"
1416

1517
"github.com/code-payments/code-server/pkg/cache"
1618
chat_util "github.com/code-payments/code-server/pkg/code/chat"
@@ -21,8 +23,10 @@ import (
2123
"github.com/code-payments/code-server/pkg/code/data/deposit"
2224
"github.com/code-payments/code-server/pkg/code/data/fulfillment"
2325
"github.com/code-payments/code-server/pkg/code/data/intent"
26+
"github.com/code-payments/code-server/pkg/code/data/onramp"
2427
"github.com/code-payments/code-server/pkg/code/data/transaction"
2528
"github.com/code-payments/code-server/pkg/code/push"
29+
"github.com/code-payments/code-server/pkg/code/thirdparty"
2630
currency_lib "github.com/code-payments/code-server/pkg/currency"
2731
"github.com/code-payments/code-server/pkg/database/query"
2832
"github.com/code-payments/code-server/pkg/kin"
@@ -252,7 +256,19 @@ func processPotentialExternalDeposit(ctx context.Context, conf *conf, data code_
252256
}
253257

254258
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...)
256272
if err != nil {
257273
return errors.Wrap(err, "error creating chat message")
258274
}
@@ -452,6 +468,185 @@ func getCodeSwapMetadata(ctx context.Context, conf *conf, tokenBalances *solana.
452468
return true, usdcAccount, usdcPaid, nil
453469
}
454470

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+
455650
// Optimistically tries to cache a balance for an external account not managed
456651
// Code. It doesn't need to be perfect and will be lazily corrected on the next
457652
// balance fetch with a newer state returned by a RPC node.

pkg/code/chat/message_code_team.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@ import (
77
"github.com/pkg/errors"
88

99
chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1"
10-
"github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
10+
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
1111

1212
"github.com/code-payments/code-server/pkg/code/common"
1313
code_data "github.com/code-payments/code-server/pkg/code/data"
1414
"github.com/code-payments/code-server/pkg/code/data/chat"
1515
"github.com/code-payments/code-server/pkg/code/data/intent"
1616
"github.com/code-payments/code-server/pkg/code/localization"
17-
currency_lib "github.com/code-payments/code-server/pkg/currency"
18-
"github.com/code-payments/code-server/pkg/usdc"
1917
)
2018

2119
// SendCodeTeamMessage sends a message to the Code Team chat.
@@ -83,7 +81,11 @@ func NewUsdcBeingConvertedMessage() (*chatpb.ChatMessage, error) {
8381

8482
// ToKinAvailableForUseMessage turns details of a USDC swap transaction into a
8583
// chat message to be inserted into the Code Team chat.
86-
func ToKinAvailableForUseMessage(signature string, usdcQuarksSwapped uint64, ts time.Time) (*chatpb.ChatMessage, error) {
84+
func ToKinAvailableForUseMessage(signature string, ts time.Time, purchases ...*transactionpb.ExchangeDataWithoutRate) (*chatpb.ChatMessage, error) {
85+
if len(purchases) == 0 {
86+
return nil, errors.New("no purchases for kin available chat message")
87+
}
88+
8789
content := []*chatpb.Content{
8890
{
8991
Type: &chatpb.Content_Localized{
@@ -92,19 +94,18 @@ func ToKinAvailableForUseMessage(signature string, usdcQuarksSwapped uint64, ts
9294
},
9395
},
9496
},
95-
{
97+
}
98+
for _, purchase := range purchases {
99+
content = append(content, &chatpb.Content{
96100
Type: &chatpb.Content_ExchangeData{
97101
ExchangeData: &chatpb.ExchangeDataContent{
98102
Verb: chatpb.ExchangeDataContent_PURCHASED,
99103
ExchangeData: &chatpb.ExchangeDataContent_Partial{
100-
Partial: &transaction.ExchangeDataWithoutRate{
101-
Currency: string(currency_lib.USD),
102-
NativeAmount: float64(usdcQuarksSwapped) / float64(usdc.QuarksPerUsdc),
103-
},
104+
Partial: purchase,
104105
},
105106
},
106107
},
107-
},
108+
})
108109
}
109110
return newProtoChatMessage(signature, content, ts)
110111
}

pkg/code/server/grpc/transaction/v2/swap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func (s *transactionServer) Swap(streamer transactionpb.Transaction_SwapServer)
153153
if initiateReq.Limit == 0 {
154154
amountToSwap = swapSourceBalance
155155
} else {
156-
amountToSwap = initiateReq.Limit
156+
return handleSwapError(streamer, status.Error(codes.InvalidArgument, "only unlimited swap is supported"))
157157
}
158158
if amountToSwap == 0 {
159159
return handleSwapError(streamer, newSwapValidationError("usdc account balance is 0"))

0 commit comments

Comments
 (0)