From 189692f972af54c770895d1d7fbba45b77bd7e34 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 24 Apr 2025 14:18:22 -0400 Subject: [PATCH] Implement remote send for the VM --- go.mod | 2 +- go.sum | 4 +- pkg/code/async/account/gift_card.go | 255 ++++++++------ pkg/code/async/account/gift_card_test.go | 15 +- pkg/code/async/account/service.go | 16 +- pkg/code/async/account/testutil.go | 72 ++-- pkg/code/async/sequencer/intent_handler.go | 33 +- pkg/code/async/sequencer/metrics.go | 13 +- pkg/code/data/action/memory/store.go | 36 +- pkg/code/data/action/memory/store_test.go | 2 +- pkg/code/data/action/postgres/model.go | 10 +- pkg/code/data/action/tests/tests.go | 30 +- pkg/code/data/fulfillment/memory/store.go | 3 +- pkg/code/data/fulfillment/postgres/model.go | 2 +- pkg/code/data/fulfillment/tests/tests.go | 8 +- pkg/code/data/intent/intent.go | 21 +- pkg/code/data/intent/memory/store.go | 69 ++-- pkg/code/data/intent/postgres/model.go | 10 +- pkg/code/data/intent/postgres/store.go | 29 +- pkg/code/data/intent/tests/tests.go | 115 +++---- pkg/code/server/account/server.go | 39 +-- pkg/code/server/account/server_test.go | 76 +---- pkg/code/server/transaction/action_handler.go | 60 ++-- pkg/code/server/transaction/intent.go | 28 +- pkg/code/server/transaction/intent_handler.go | 315 +++++++----------- .../server/transaction/local_simulation.go | 99 +++--- pkg/pointer/pointer.go | 30 ++ 27 files changed, 670 insertions(+), 722 deletions(-) diff --git a/go.mod b/go.mod index ead8d0a4..0bf7d5ec 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.0 require ( github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.19.1-0.20250416185804-64d2132a62f2 + github.com/code-payments/code-protobuf-api v1.19.1-0.20250423160200-d52845b4298f github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.2.1 diff --git a/go.sum b/go.sum index 5c143a1a..3b8abc50 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.19.1-0.20250416185804-64d2132a62f2 h1:L2nVJRDyqbfx9DUUD7B0Es9JICjdwCjrJOdtmxNwAcs= -github.com/code-payments/code-protobuf-api v1.19.1-0.20250416185804-64d2132a62f2/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs= +github.com/code-payments/code-protobuf-api v1.19.1-0.20250423160200-d52845b4298f h1:bSvfqD5gy9n/xrYtALu1yV5oBEDf06mi/mkDkvBS29A= +github.com/code-payments/code-protobuf-api v1.19.1-0.20250423160200-d52845b4298f/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs= github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba h1:Bkp+gmeb6Y2PWXfkSCTMBGWkb2P1BujRDSjWeI+0j5I= github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba/go.mod h1:jSiifpiBpyBQ8q2R0MGEbkSgWC6sbdRTyDBntmW+j1E= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= diff --git a/pkg/code/async/account/gift_card.go b/pkg/code/async/account/gift_card.go index ed6fd6eb..21922494 100644 --- a/pkg/code/async/account/gift_card.go +++ b/pkg/code/async/account/gift_card.go @@ -2,10 +2,13 @@ package async_account import ( "context" + "crypto/sha256" "errors" + "math" "sync" "time" + "github.com/mr-tron/base58" "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" @@ -15,13 +18,14 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" + "github.com/code-payments/code-server/pkg/code/data/fulfillment" + "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/metrics" + "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/retry" ) -// todo: Is this even relevant anymore with the VM? If so, we need new logic because -// closing dormant accounts is no longer a thing with the VM. - const ( giftCardAutoReturnIntentPrefix = "auto-return-gc-" giftCardExpiry = 24 * time.Hour @@ -39,7 +43,8 @@ func (p *service) giftCardAutoReturnWorker(serviceCtx context.Context, interval defer m.End() tracedCtx := newrelic.NewContext(serviceCtx, m) - records, err := p.data.GetPrioritizedAccountInfosRequiringAutoReturnCheck(tracedCtx, giftCardExpiry, 10) + // todo: configurable batch size + records, err := p.data.GetPrioritizedAccountInfosRequiringAutoReturnCheck(tracedCtx, giftCardExpiry, 32) if err == account.ErrAccountInfoNotFound { return nil } else if err != nil { @@ -91,13 +96,14 @@ func (p *service) maybeInitiateGiftCardAutoReturn(ctx context.Context, accountIn if err == nil { log.Trace("gift card is claimed and will be removed from worker queue") - // Gift card is claimed, so take it out of the worker queue. The auto-return - // action and fulfillment will be revoked in the fulfillment worker from generic - // account closing flows. - // - // Note: It is possible the original issuer "claimed" the gift card. This is - // actually ideal because funds move in a more private manner through the - // temp incoming account versus the primary account. + // Cleanup anything related to gift card auto-return, since it cannot be scheduled + err = p.initiateProcessToCleanupGiftCardAutoReturn(ctx, giftCardVaultAccount) + if err != nil { + log.WithError(err).Warn("failure cleaning up auto-return action") + return err + } + + // Gift card is claimed, so take it out of the worker queue. return markAutoReturnCheckComplete(ctx, p.data, accountInfoRecord) } else if err != action.ErrActionNotFound { return err @@ -128,67 +134,82 @@ func (p *service) maybeInitiateGiftCardAutoReturn(ctx context.Context, accountIn // Note: This is the first instance of handling a conditional action, and could be // a good guide for similar actions in the future. func (p *service) initiateProcessToAutoReturnGiftCard(ctx context.Context, giftCardVaultAccount *common.Account) error { - /* - giftCardIssuedIntent, err := p.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVaultAccount.PublicKey().ToBase58()) - if err != nil { - return err - } + giftCardIssuedIntent, err := p.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err != nil { + return err + } - autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) - if err != nil { - return err - } + autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err != nil { + return err + } - autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId) - if err != nil { - return err - } + autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId) + if err != nil { + return err + } - // Add a payment history item to show the funds being returned back to the issuer - err = insertAutoReturnPaymentHistoryItem(ctx, p.data, giftCardIssuedIntent) - if err != nil { - return err - } + // Add a intent record to show the funds being returned back to the issuer + err = insertAutoReturnIntentRecord(ctx, p.data, giftCardIssuedIntent) + if err != nil { + return err + } - // We need to update pre-sorting because close dormant fulfillments are always - // inserted at the very last spot in the line. - // - // Must be the first thing to succeed! We cannot risk a deposit back into the - // organizer to win a race in scheduling. By pre-sorting this to the end of - // the gift card issued intent, we ensure the auto-return is blocked on any - // fulfillments to setup the gift card. We'll also guarantee that subsequent - // intents that utilize the primary account as a source of funds will be blocked - // by the auto-return. - err = updateCloseDormantAccountFulfillmentPreSorting( - ctx, - p.data, - autoReturnFulfillment[0], - giftCardIssuedIntent.Id, - math.MaxInt32, - 0, - ) - if err != nil { - return err - } + // We need to update pre-sorting because auto-return fulfillments are always + // inserted at the very last spot in the line. + // + // Must be the first thing to succeed! By pre-sorting this to the end of + // the gift card issued intent, we ensure the auto-return is blocked on any + // fulfillments to setup the gift card. We'll also guarantee that subsequent + // intents that utilize the primary account as a source of funds will be blocked + // by the auto-return. + err = updateAutoReturnFulfillmentPreSorting( + ctx, + p.data, + autoReturnFulfillment[0], + giftCardIssuedIntent.Id, + math.MaxInt32, + 0, + ) + if err != nil { + return err + } - // This will update the action's quantity, so balance changes are reflected. We - // also unblock fulfillment scheduling by moving the action out of the unknown - // state and into the pending state. - err = scheduleCloseDormantAccountAction( - ctx, - p.data, - autoReturnAction, - giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity, - ) - if err != nil { - return err - } + // This will update the action's quantity, so balance changes are reflected. We + // also unblock fulfillment scheduling by moving the action out of the unknown + // state and into the pending state. + err = scheduleAutoReturnAction( + ctx, + p.data, + autoReturnAction, + giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity, + ) + if err != nil { + return err + } + + // This will trigger the fulfillment worker to poll for the fulfillment. This + // should be the very last DB update called. + return markFulfillmentAsActivelyScheduled(ctx, p.data, autoReturnFulfillment[0]) +} - // This will trigger the fulfillment worker to poll for the fulfillment. This - // should be the very last DB update called. - return markFulfillmentAsActivelyScheduled(ctx, p.data, autoReturnFulfillment[0]) - */ - return errors.New("requires rewrite") +func (p *service) initiateProcessToCleanupGiftCardAutoReturn(ctx context.Context, giftCardVaultAccount *common.Account) error { + autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err != nil { + return err + } + + autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId) + if err != nil { + return err + } + + err = markActionAsRevoked(ctx, p.data, autoReturnAction) + if err != nil { + return err + } + + return markFulfillmentAsRevoked(ctx, p.data, autoReturnFulfillment[0]) } func markAutoReturnCheckComplete(ctx context.Context, data code_data.Provider, record *account.Record) error { @@ -200,14 +221,12 @@ func markAutoReturnCheckComplete(ctx context.Context, data code_data.Provider, r return data.UpdateAccountInfo(ctx, record) } -/* - // Note: Structured like a generic utility because it could very well evolve // into that, but there's no reason to call this on anything else as of // writing this comment. -func scheduleCloseDormantAccountAction(ctx context.Context, data code_data.Provider, actionRecord *action.Record, balance uint64) error { - if actionRecord.ActionType != action.CloseDormantAccount { - return errors.New("expected a close dormant account action") +func scheduleAutoReturnAction(ctx context.Context, data code_data.Provider, actionRecord *action.Record, balance uint64) error { + if actionRecord.ActionType != action.NoPrivacyWithdraw { + return errors.New("expected a no privacy withdraw action") } if actionRecord.State == action.StatePending { @@ -226,7 +245,7 @@ func scheduleCloseDormantAccountAction(ctx context.Context, data code_data.Provi // Note: Structured like a generic utility because it could very well evolve // into that, but there's no reason to call this on anything else as of // writing this comment. -func updateCloseDormantAccountFulfillmentPreSorting( +func updateAutoReturnFulfillmentPreSorting( ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record, @@ -234,8 +253,8 @@ func updateCloseDormantAccountFulfillmentPreSorting( actionOrderingIndex uint32, fulfillmentOrderingIndex uint32, ) error { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseDormantTimelockAccount { - return errors.New("expected a close dormant timelock account fulfillment") + if fulfillmentRecord.FulfillmentType != fulfillment.NoPrivacyWithdraw { + return errors.New("expected a no privacy withdraw fulfillment") } if fulfillmentRecord.IntentOrderingIndex == intentOrderingIndex && @@ -254,24 +273,7 @@ func updateCloseDormantAccountFulfillmentPreSorting( return data.UpdateFulfillment(ctx, fulfillmentRecord) } -func markFulfillmentAsActivelyScheduled(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) error { - if fulfillmentRecord.Id == 0 { - return errors.New("fulfillment id is zero") - } - - if !fulfillmentRecord.DisableActiveScheduling { - return nil - } - - if fulfillmentRecord.State != fulfillment.StateUnknown { - return errors.New("expected fulfillment in unknown state") - } - - // Note: different than Save, since we don't have distributed locks - return data.MarkFulfillmentAsActivelyScheduled(ctx, fulfillmentRecord.Id) -} - -func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Provider, giftCardIssuedIntent *intent.Record) error { +func insertAutoReturnIntentRecord(ctx context.Context, data code_data.Provider, giftCardIssuedIntent *intent.Record) error { usdExchangeRecord, err := data.GetExchangeRate(ctx, currency.USD, time.Now()) if err != nil { return err @@ -280,12 +282,6 @@ func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Prov // We need to insert a faked completed public receive intent so it can appear // as a return in the user's payment history. Think of it as a server-initiated // intent on behalf of the user based on pre-approved conditional actions. - // - // Deprecated in favour of chats (for history purposes) - // - // todo: Should we remap the CloseDormantAccount action and fulfillments, then - // tie the fulfillment/action state to the intent state? Just doing the - // easiest thing for now to get auto-return out the door. intentRecord := &intent.Record{ IntentId: getAutoReturnIntentId(giftCardIssuedIntent.IntentId), IntentType: intent.ReceivePaymentsPublicly, @@ -293,16 +289,17 @@ func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Prov InitiatorOwnerAccount: giftCardIssuedIntent.InitiatorOwnerAccount, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{ - Source: giftCardIssuedIntent.SendPrivatePaymentMetadata.DestinationTokenAccount, - Quantity: giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity, + Source: giftCardIssuedIntent.SendPublicPaymentMetadata.DestinationTokenAccount, + Quantity: giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity, + IsRemoteSend: true, IsReturned: true, - OriginalExchangeCurrency: giftCardIssuedIntent.SendPrivatePaymentMetadata.ExchangeCurrency, - OriginalExchangeRate: giftCardIssuedIntent.SendPrivatePaymentMetadata.ExchangeRate, - OriginalNativeAmount: giftCardIssuedIntent.SendPrivatePaymentMetadata.NativeAmount, + OriginalExchangeCurrency: giftCardIssuedIntent.SendPublicPaymentMetadata.ExchangeCurrency, + OriginalExchangeRate: giftCardIssuedIntent.SendPublicPaymentMetadata.ExchangeRate, + OriginalNativeAmount: giftCardIssuedIntent.SendPublicPaymentMetadata.NativeAmount, - UsdMarketValue: usdExchangeRecord.Rate * float64(common.FromCoreMintQuarks(giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity)), + UsdMarketValue: usdExchangeRecord.Rate * float64(giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity) / float64(common.CoreMintQuarksPerUnit), }, State: intent.StateConfirmed, @@ -312,10 +309,58 @@ func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Prov return data.SaveIntent(ctx, intentRecord) } +func markActionAsRevoked(ctx context.Context, data code_data.Provider, actionRecord *action.Record) error { + if actionRecord.State == action.StateRevoked { + return nil + } + + if actionRecord.State != action.StateUnknown { + return errors.New("expected fulfillment in unknown state") + } + + actionRecord.State = action.StateRevoked + + return data.UpdateAction(ctx, actionRecord) +} + +func markFulfillmentAsActivelyScheduled(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) error { + if fulfillmentRecord.Id == 0 { + return errors.New("fulfillment id is zero") + } + + if !fulfillmentRecord.DisableActiveScheduling { + return nil + } + + if fulfillmentRecord.State != fulfillment.StateUnknown { + return errors.New("expected fulfillment in unknown state") + } + + // Note: different than Save, since we don't have distributed locks + return data.MarkFulfillmentAsActivelyScheduled(ctx, fulfillmentRecord.Id) +} + +func markFulfillmentAsRevoked(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) error { + if fulfillmentRecord.Id == 0 { + return errors.New("fulfillment id is zero") + } + + if fulfillmentRecord.State == fulfillment.StateRevoked { + return nil + } + + if fulfillmentRecord.State != fulfillment.StateUnknown { + return errors.New("expected fulfillment in unknown state") + } + + fulfillmentRecord.State = fulfillment.StateRevoked + + return data.UpdateFulfillment(ctx, fulfillmentRecord) +} + // Must be unique, but consistent for idempotency, and ideally fit in a 32 // byte buffer. func getAutoReturnIntentId(originalIntentId string) string { hashed := sha256.Sum256([]byte(giftCardAutoReturnIntentPrefix + originalIntentId)) return base58.Encode(hashed[:]) } -*/ diff --git a/pkg/code/async/account/gift_card_test.go b/pkg/code/async/account/gift_card_test.go index d380dcc2..d33a6483 100644 --- a/pkg/code/async/account/gift_card_test.go +++ b/pkg/code/async/account/gift_card_test.go @@ -1,6 +1,15 @@ package async_account -/* +import ( + "testing" + "time" + + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/code-payments/code-server/pkg/testutil" +) func TestGiftCardAutoReturn_ExpiryWindow(t *testing.T) { for _, tc := range []struct { @@ -33,7 +42,7 @@ func TestGiftCardAutoReturn_ExpiryWindow(t *testing.T) { if tc.isAutoReturned { env.assertGiftCardAutoReturned(t, giftCard) } else { - env.assertGiftCardNotAutoReturned(t, giftCard, tc.isAutoReturned) + env.assertGiftCardNotAutoReturned(t, giftCard, false) } } } @@ -76,5 +85,3 @@ func TestGiftCardAutoReturn_IntentId(t *testing.T) { assert.Equal(t, generated2, getAutoReturnIntentId(intentId2)) } } - -*/ diff --git a/pkg/code/async/account/service.go b/pkg/code/async/account/service.go index bed46ed9..201bd1f4 100644 --- a/pkg/code/async/account/service.go +++ b/pkg/code/async/account/service.go @@ -25,15 +25,13 @@ func New(data code_data.Provider, configProvider ConfigProvider) async.Service { } func (p *service) Start(ctx context.Context, interval time.Duration) error { - // todo: auto returns are broken because we've removed close dormant account actions - /* - go func() { - err := p.giftCardAutoReturnWorker(ctx, interval) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("gift card auto-return processing loop terminated unexpectedly") - } - }() - */ + + go func() { + err := p.giftCardAutoReturnWorker(ctx, interval) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("gift card auto-return processing loop terminated unexpectedly") + } + }() // todo: the open code protocol needs to get the push token from the implementing app /* diff --git a/pkg/code/async/account/testutil.go b/pkg/code/async/account/testutil.go index 2e109276..e6235ecd 100644 --- a/pkg/code/async/account/testutil.go +++ b/pkg/code/async/account/testutil.go @@ -1,6 +1,5 @@ package async_account -/* import ( "context" "math" @@ -32,10 +31,10 @@ type testEnv struct { type testGiftCard struct { accountInfoRecord *account.Record - issuedIntentRecord *intent.Record - claimedActionRecord *action.Record - closeDormantActionRecord *action.Record - closeDormantFulfillmentRecord *fulfillment.Record + issuedIntentRecord *intent.Record + claimedActionRecord *action.Record + autoReturnActionRecord *action.Record + autoReturnFulfillmentRecord *fulfillment.Record } func setup(t *testing.T) *testEnv { @@ -80,11 +79,11 @@ func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *te intentRecord := &intent.Record{ IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.SendPrivatePayment, + IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ + SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ DestinationTokenAccount: accountInfoRecord.TokenAccount, Quantity: common.ToCoreMintQuarks(12345), @@ -102,12 +101,12 @@ func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *te } require.NoError(t, e.data.SaveIntent(e.ctx, intentRecord)) - closeDormantActionRecord := &action.Record{ + autoReturnActionRecord := &action.Record{ Intent: intentRecord.IntentId, IntentType: intentRecord.IntentType, ActionId: 10, - ActionType: action.CloseDormantAccount, + ActionType: action.NoPrivacyWithdraw, Source: accountInfoRecord.TokenAccount, Destination: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), @@ -117,24 +116,24 @@ func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *te CreatedAt: creationTs, } - require.NoError(t, e.data.PutAllActions(e.ctx, closeDormantActionRecord)) + require.NoError(t, e.data.PutAllActions(e.ctx, autoReturnActionRecord)) - closeDormantFulfillmentRecord := &fulfillment.Record{ + autoReturnFulfillmentRecord := &fulfillment.Record{ Intent: intentRecord.IntentId, IntentType: intentRecord.IntentType, - ActionId: closeDormantActionRecord.ActionId, - ActionType: closeDormantActionRecord.ActionType, + ActionId: autoReturnActionRecord.ActionId, + ActionType: autoReturnActionRecord.ActionType, - FulfillmentType: fulfillment.CloseDormantTimelockAccount, + FulfillmentType: fulfillment.NoPrivacyWithdraw, Data: []byte("data"), Signature: pointer.String(testutil.NewRandomAccount(t).PrivateKey().ToBase58()), Nonce: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), Blockhash: pointer.String("bh"), - Source: closeDormantActionRecord.Source, - Destination: pointer.StringCopy(closeDormantActionRecord.Destination), + Source: autoReturnActionRecord.Source, + Destination: pointer.StringCopy(autoReturnActionRecord.Destination), IntentOrderingIndex: math.MaxInt64, ActionOrderingIndex: 0, @@ -146,14 +145,14 @@ func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *te CreatedAt: creationTs, } - require.NoError(t, e.data.PutAllFulfillments(e.ctx, closeDormantFulfillmentRecord)) + require.NoError(t, e.data.PutAllFulfillments(e.ctx, autoReturnFulfillmentRecord)) return &testGiftCard{ accountInfoRecord: accountInfoRecord, - issuedIntentRecord: intentRecord, - closeDormantActionRecord: closeDormantActionRecord, - closeDormantFulfillmentRecord: closeDormantFulfillmentRecord, + issuedIntentRecord: intentRecord, + autoReturnActionRecord: autoReturnActionRecord, + autoReturnFulfillmentRecord: autoReturnFulfillmentRecord, } } @@ -169,7 +168,7 @@ func (e *testEnv) simulateGiftCardBeingClaimed(t *testing.T, giftCard *testGiftC Source: giftCard.accountInfoRecord.TokenAccount, Destination: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - Quantity: pointer.Uint64(giftCard.issuedIntentRecord.SendPrivatePaymentMetadata.Quantity), + Quantity: pointer.Uint64(giftCard.issuedIntentRecord.SendPublicPaymentMetadata.Quantity), State: action.StatePending, } @@ -181,13 +180,13 @@ func (e *testEnv) assertGiftCardAutoReturned(t *testing.T, giftCard *testGiftCar require.NoError(t, err) assert.False(t, accountInfoRecord.RequiresAutoReturnCheck) - actionRecord, err := e.data.GetActionById(e.ctx, giftCard.closeDormantActionRecord.Intent, giftCard.closeDormantActionRecord.ActionId) + actionRecord, err := e.data.GetActionById(e.ctx, giftCard.autoReturnActionRecord.Intent, giftCard.autoReturnActionRecord.ActionId) require.NoError(t, err) require.NotNil(t, actionRecord.Quantity) - assert.Equal(t, giftCard.issuedIntentRecord.SendPrivatePaymentMetadata.Quantity, *actionRecord.Quantity) + assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.Quantity, *actionRecord.Quantity) assert.Equal(t, action.StatePending, actionRecord.State) - fulfillmentRecord, err := e.data.GetFulfillmentBySignature(e.ctx, *giftCard.closeDormantFulfillmentRecord.Signature) + fulfillmentRecord, err := e.data.GetFulfillmentBySignature(e.ctx, *giftCard.autoReturnFulfillmentRecord.Signature) require.NoError(t, err) assert.EqualValues(t, giftCard.issuedIntentRecord.Id, fulfillmentRecord.IntentOrderingIndex) assert.EqualValues(t, math.MaxInt32, fulfillmentRecord.ActionOrderingIndex) @@ -203,12 +202,12 @@ func (e *testEnv) assertGiftCardAutoReturned(t *testing.T, giftCard *testGiftCar assert.Equal(t, giftCard.issuedIntentRecord.InitiatorOwnerAccount, historyRecord.InitiatorOwnerAccount) require.NotNil(t, historyRecord.ReceivePaymentsPubliclyMetadata) assert.Equal(t, giftCard.accountInfoRecord.TokenAccount, historyRecord.ReceivePaymentsPubliclyMetadata.Source) - assert.Equal(t, giftCard.issuedIntentRecord.SendPrivatePaymentMetadata.Quantity, historyRecord.ReceivePaymentsPubliclyMetadata.Quantity) + assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.Quantity, historyRecord.ReceivePaymentsPubliclyMetadata.Quantity) assert.True(t, historyRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend) assert.True(t, historyRecord.ReceivePaymentsPubliclyMetadata.IsReturned) - assert.Equal(t, giftCard.issuedIntentRecord.SendPrivatePaymentMetadata.ExchangeCurrency, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeCurrency) - assert.Equal(t, giftCard.issuedIntentRecord.SendPrivatePaymentMetadata.ExchangeRate, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate) - assert.Equal(t, giftCard.issuedIntentRecord.SendPrivatePaymentMetadata.NativeAmount, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount) + assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.ExchangeCurrency, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeCurrency) + assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.ExchangeRate, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate) + assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.NativeAmount, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount) assert.Equal(t, 1234.5, historyRecord.ReceivePaymentsPubliclyMetadata.UsdMarketValue) assert.Equal(t, intent.StateConfirmed, historyRecord.State) } @@ -218,20 +217,27 @@ func (e *testEnv) assertGiftCardNotAutoReturned(t *testing.T, giftCard *testGift require.NoError(t, err) assert.Equal(t, isRemovedFromWorkerQueue, !accountInfoRecord.RequiresAutoReturnCheck) - actionRecord, err := e.data.GetActionById(e.ctx, giftCard.closeDormantActionRecord.Intent, giftCard.closeDormantActionRecord.ActionId) + actionRecord, err := e.data.GetActionById(e.ctx, giftCard.autoReturnActionRecord.Intent, giftCard.autoReturnActionRecord.ActionId) require.NoError(t, err) assert.Nil(t, actionRecord.Quantity) - assert.Equal(t, action.StateUnknown, actionRecord.State) + if isRemovedFromWorkerQueue { + assert.Equal(t, action.StateRevoked, actionRecord.State) + } else { + assert.Equal(t, action.StateUnknown, actionRecord.State) + } - fulfillmentRecord, err := e.data.GetFulfillmentBySignature(e.ctx, *giftCard.closeDormantFulfillmentRecord.Signature) + fulfillmentRecord, err := e.data.GetFulfillmentBySignature(e.ctx, *giftCard.autoReturnFulfillmentRecord.Signature) require.NoError(t, err) assert.EqualValues(t, math.MaxInt64, fulfillmentRecord.IntentOrderingIndex) assert.EqualValues(t, 0, fulfillmentRecord.ActionOrderingIndex) assert.EqualValues(t, 0, fulfillmentRecord.FulfillmentOrderingIndex) assert.True(t, fulfillmentRecord.DisableActiveScheduling) - assert.Equal(t, fulfillment.StateUnknown, fulfillmentRecord.State) + if isRemovedFromWorkerQueue { + assert.Equal(t, fulfillment.StateRevoked, fulfillmentRecord.State) + } else { + assert.Equal(t, fulfillment.StateUnknown, fulfillmentRecord.State) + } _, err = e.data.GetIntent(e.ctx, giftCardAutoReturnIntentPrefix+giftCard.issuedIntentRecord.IntentId) assert.Equal(t, intent.ErrIntentNotFound, err) } -*/ diff --git a/pkg/code/async/sequencer/intent_handler.go b/pkg/code/async/sequencer/intent_handler.go index 4c69b3f6..99ae91a0 100644 --- a/pkg/code/async/sequencer/intent_handler.go +++ b/pkg/code/async/sequencer/intent_handler.go @@ -67,18 +67,39 @@ func NewSendPublicPaymentIntentHandler(data code_data.Provider) IntentHandler { } func (h *SendPublicPaymentIntentHandler) OnActionUpdated(ctx context.Context, intentId string) error { - actionRecord, err := h.data.GetActionById(ctx, intentId, 0) + actionRecords, err := h.data.GetAllActionsByIntent(ctx, intentId) if err != nil { return err } - // Intent is confirmed/failed based on the state the single action - switch actionRecord.State { - case action.StateConfirmed: - return markIntentConfirmed(ctx, h.data, intentId) - case action.StateFailed: + actionRecordsToCheck := actionRecords + if len(actionRecords) > 1 { + // Do not include the auto-return action, which is a different server-side + // initiated intent using the final action here. + // + // todo: Assumes > 1 case is just remote send + actionRecordsToCheck = actionRecordsToCheck[:len(actionRecordsToCheck)-1] + } + + allConfirmed := true + anyFailures := false + for _, actionRecord := range actionRecordsToCheck { + switch actionRecord.State { + case action.StateConfirmed: + case action.StateFailed: + anyFailures = true + fallthrough + default: + allConfirmed = false + } + } + + if anyFailures { return markIntentFailed(ctx, h.data, intentId) } + if allConfirmed { + return markIntentConfirmed(ctx, h.data, intentId) + } return nil } diff --git a/pkg/code/async/sequencer/metrics.go b/pkg/code/async/sequencer/metrics.go index 69ea0b63..f8558905 100644 --- a/pkg/code/async/sequencer/metrics.go +++ b/pkg/code/async/sequencer/metrics.go @@ -4,15 +4,14 @@ import ( "context" "time" - "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/fulfillment" + "github.com/code-payments/code-server/pkg/metrics" ) const ( - fulfillmentCountEventName = "FulfillmentCountPollingCheck" - subsidizerBalanceEventName = "SubsidizerBalancePollingCheck" - temporaryPrivateTransferScheduledEventName = "TemporaryPrivateTransferScheduled" + fulfillmentCountEventName = "FulfillmentCountPollingCheck" + subsidizerBalanceEventName = "SubsidizerBalancePollingCheck" ) func (p *service) metricsGaugeWorker(ctx context.Context) error { @@ -63,9 +62,3 @@ func recordSubsidizerBalanceEvent(ctx context.Context, lamports uint64) { "lamports": lamports, }) } - -func recordTemporaryPrivateTransferScheduledEvent(ctx context.Context, fulfillmentRecord *fulfillment.Record) { - metrics.RecordEvent(ctx, temporaryPrivateTransferScheduledEventName, map[string]interface{}{ - "signature": *fulfillmentRecord.Signature, - }) -} diff --git a/pkg/code/data/action/memory/store.go b/pkg/code/data/action/memory/store.go index 9200b270..29223cca 100644 --- a/pkg/code/data/action/memory/store.go +++ b/pkg/code/data/action/memory/store.go @@ -3,12 +3,11 @@ package memory import ( "context" "fmt" - "sort" "sync" "time" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/database/query" + "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/pointer" ) @@ -90,35 +89,14 @@ func (s *store) findBySource(source string) []*action.Record { return res } -func (s *store) filter(items []*action.Record, cursor query.Cursor, limit uint64, direction query.Ordering) []*action.Record { - var start uint64 - - start = 0 - if direction == query.Descending { - start = s.last + 1 - } - if len(cursor) > 0 { - start = cursor.ToUint64() - } - +func (s *store) filterByIntentType(items []*action.Record, want intent.Type) []*action.Record { var res []*action.Record for _, item := range items { - if item.Id > start && direction == query.Ascending { - res = append(res, item) - } - if item.Id < start && direction == query.Descending { + if item.IntentType == want { res = append(res, item) + continue } } - - if direction == query.Descending { - sort.Sort(sort.Reverse(ById(res))) - } - - if len(res) >= int(limit) { - return res[:limit] - } - return res } @@ -197,7 +175,7 @@ func (s *store) Update(ctx context.Context, record *action.Record) error { defer s.mu.Unlock() if item := s.find(record); item != nil { - if record.ActionType == action.CloseDormantAccount { + if record.IntentType == intent.SendPublicPayment && record.ActionType == action.NoPrivacyWithdraw { item.Quantity = pointer.Uint64Copy(record.Quantity) } item.State = record.State @@ -284,6 +262,7 @@ func (s *store) GetGiftCardClaimedAction(ctx context.Context, giftCardVault stri items := s.findBySource(giftCardVault) items = s.filterByActionType(items, action.NoPrivacyWithdraw) + items = s.filterByIntentType(items, intent.ReceivePaymentsPublicly) items = s.filterByState(items, false, action.StateRevoked) if len(items) == 0 { @@ -302,7 +281,8 @@ func (s *store) GetGiftCardAutoReturnAction(ctx context.Context, giftCardVault s defer s.mu.Unlock() items := s.findBySource(giftCardVault) - items = s.filterByActionType(items, action.CloseDormantAccount) + items = s.filterByActionType(items, action.NoPrivacyWithdraw) + items = s.filterByIntentType(items, intent.SendPublicPayment) items = s.filterByState(items, false, action.StateRevoked) if len(items) == 0 { diff --git a/pkg/code/data/action/memory/store_test.go b/pkg/code/data/action/memory/store_test.go index 56e03b21..20e1516b 100644 --- a/pkg/code/data/action/memory/store_test.go +++ b/pkg/code/data/action/memory/store_test.go @@ -6,7 +6,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/action/tests" ) -func TestTreasuryPoolMemoryStore(t *testing.T) { +func TestActionMemoryStore(t *testing.T) { testStore := New() teardown := func() { testStore.(*store).reset() diff --git a/pkg/code/data/action/postgres/model.go b/pkg/code/data/action/postgres/model.go index 85d47d18..fe1cc96a 100644 --- a/pkg/code/data/action/postgres/model.go +++ b/pkg/code/data/action/postgres/model.go @@ -96,7 +96,7 @@ func (m *model) dbUpdate(ctx context.Context, db *sqlx.DB) error { m.State, } - if m.ActionType == uint(action.CloseDormantAccount) { + if m.IntentType == uint(intent.SendPublicPayment) && m.ActionType == uint(action.NoPrivacyWithdraw) { quantityUpdateStmt = ", quantity = $4" params = append(params, m.Quantity) } @@ -299,7 +299,7 @@ func dbGetGiftCardClaimedAction(ctx context.Context, db *sqlx.DB, giftCardVault query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at FROM ` + tableName + ` - WHERE source = $1 AND action_type = $2 AND state != $3 + WHERE source = $1 AND action_type = $2 AND intent_type = $3 AND state != $4 LIMIT 2` err := db.SelectContext( @@ -308,6 +308,7 @@ func dbGetGiftCardClaimedAction(ctx context.Context, db *sqlx.DB, giftCardVault query, giftCardVault, action.NoPrivacyWithdraw, + intent.ReceivePaymentsPublicly, action.StateRevoked, ) if err != nil { @@ -328,7 +329,7 @@ func dbGetGiftCardAutoReturnAction(ctx context.Context, db *sqlx.DB, giftCardVau query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at FROM ` + tableName + ` - WHERE source = $1 AND action_type = $2 AND state != $3 + WHERE source = $1 AND action_type = $2 AND intent_type = $3 AND state != $4 LIMIT 2` err := db.SelectContext( @@ -336,7 +337,8 @@ func dbGetGiftCardAutoReturnAction(ctx context.Context, db *sqlx.DB, giftCardVau &res, query, giftCardVault, - action.CloseDormantAccount, + action.NoPrivacyWithdraw, + intent.SendPublicPayment, action.StateRevoked, ) if err != nil { diff --git a/pkg/code/data/action/tests/tests.go b/pkg/code/data/action/tests/tests.go index 3103ff47..73665c40 100644 --- a/pkg/code/data/action/tests/tests.go +++ b/pkg/code/data/action/tests/tests.go @@ -38,10 +38,10 @@ func testRoundTrip(t *testing.T, s action.Store) { expected := &action.Record{ Intent: "intent", - IntentType: intent.SendPrivatePayment, + IntentType: intent.SendPublicPayment, ActionId: 1, - ActionType: action.CloseDormantAccount, + ActionType: action.NoPrivacyWithdraw, Source: "source", Destination: pointer.String("destination"), @@ -338,19 +338,19 @@ func testGetGiftCardAutoReturnAction(t *testing.T, s action.Store) { ctx := context.Background() records := []*action.Record{ - {Intent: "i1", IntentType: intent.SendPrivatePayment, ActionId: 0, ActionType: action.OpenAccount, Source: "a1", State: action.StateConfirmed}, - {Intent: "i1", IntentType: intent.SendPrivatePayment, ActionId: 1, ActionType: action.PrivateTransfer, Source: "a1", Destination: pointer.String("destination"), State: action.StateConfirmed}, - {Intent: "i1", IntentType: intent.SendPrivatePayment, ActionId: 2, ActionType: action.NoPrivacyTransfer, Source: "a1", Destination: pointer.String("destination"), State: action.StateConfirmed}, - {Intent: "i1", IntentType: intent.SendPrivatePayment, ActionId: 3, ActionType: action.CloseEmptyAccount, Source: "a1", State: action.StateConfirmed}, - {Intent: "i1", IntentType: intent.SendPrivatePayment, ActionId: 4, ActionType: action.NoPrivacyWithdraw, Source: "a1", Destination: pointer.String("destination"), State: action.StatePending}, - {Intent: "i1", IntentType: intent.SendPrivatePayment, ActionId: 5, ActionType: action.CloseDormantAccount, Source: "a1", Destination: pointer.String("destination"), State: action.StateConfirmed}, - - {Intent: "i2", IntentType: intent.SendPrivatePayment, ActionId: 1, ActionType: action.CloseDormantAccount, Source: "a2", Destination: pointer.String("destination"), State: action.StateRevoked}, - {Intent: "i2", IntentType: intent.SendPrivatePayment, ActionId: 2, ActionType: action.CloseDormantAccount, Source: "other", Destination: pointer.String("a2"), State: action.StatePending}, - {Intent: "i2", IntentType: intent.SendPrivatePayment, ActionId: 0, ActionType: action.CloseDormantAccount, Source: "a2", Destination: pointer.String("destination"), State: action.StatePending}, - - {Intent: "i3", IntentType: intent.SendPrivatePayment, ActionId: 0, ActionType: action.CloseDormantAccount, Source: "a3", Destination: pointer.String("destination"), State: action.StatePending}, - {Intent: "i3", IntentType: intent.SendPrivatePayment, ActionId: 1, ActionType: action.CloseDormantAccount, Source: "a3", Destination: pointer.String("destination"), State: action.StateConfirmed}, + {Intent: "i1", IntentType: intent.SendPublicPayment, ActionId: 0, ActionType: action.OpenAccount, Source: "a1", State: action.StateConfirmed}, + {Intent: "i1", IntentType: intent.SendPublicPayment, ActionId: 1, ActionType: action.PrivateTransfer, Source: "a1", Destination: pointer.String("destination"), State: action.StateConfirmed}, + {Intent: "i1", IntentType: intent.SendPublicPayment, ActionId: 2, ActionType: action.NoPrivacyTransfer, Source: "a1", Destination: pointer.String("destination"), State: action.StateConfirmed}, + {Intent: "i1", IntentType: intent.SendPublicPayment, ActionId: 3, ActionType: action.CloseEmptyAccount, Source: "a1", State: action.StateConfirmed}, + {Intent: "i1", IntentType: intent.SendPublicPayment, ActionId: 4, ActionType: action.CloseDormantAccount, Source: "a1", Destination: pointer.String("destination"), State: action.StatePending}, + {Intent: "i1", IntentType: intent.SendPublicPayment, ActionId: 5, ActionType: action.NoPrivacyWithdraw, Source: "a1", Destination: pointer.String("destination"), State: action.StateConfirmed}, + + {Intent: "i2", IntentType: intent.SendPublicPayment, ActionId: 1, ActionType: action.NoPrivacyWithdraw, Source: "a2", Destination: pointer.String("destination"), State: action.StateRevoked}, + {Intent: "i2", IntentType: intent.SendPublicPayment, ActionId: 2, ActionType: action.NoPrivacyWithdraw, Source: "other", Destination: pointer.String("a2"), State: action.StatePending}, + {Intent: "i2", IntentType: intent.SendPublicPayment, ActionId: 0, ActionType: action.NoPrivacyWithdraw, Source: "a2", Destination: pointer.String("destination"), State: action.StatePending}, + + {Intent: "i3", IntentType: intent.SendPublicPayment, ActionId: 0, ActionType: action.NoPrivacyWithdraw, Source: "a3", Destination: pointer.String("destination"), State: action.StatePending}, + {Intent: "i3", IntentType: intent.SendPublicPayment, ActionId: 1, ActionType: action.NoPrivacyWithdraw, Source: "a3", Destination: pointer.String("destination"), State: action.StateConfirmed}, } require.NoError(t, s.PutAll(ctx, records...)) diff --git a/pkg/code/data/fulfillment/memory/store.go b/pkg/code/data/fulfillment/memory/store.go index c14c0f02..1cb08d09 100644 --- a/pkg/code/data/fulfillment/memory/store.go +++ b/pkg/code/data/fulfillment/memory/store.go @@ -8,6 +8,7 @@ import ( "time" "github.com/code-payments/code-server/pkg/code/data/fulfillment" + "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/pointer" ) @@ -515,7 +516,7 @@ func (s *store) Update(ctx context.Context, data *fulfillment.Record) error { item.State = data.State - if item.FulfillmentType == fulfillment.CloseDormantTimelockAccount { + if item.IntentType == intent.SendPublicPayment && item.FulfillmentType == fulfillment.NoPrivacyWithdraw { item.IntentOrderingIndex = data.IntentOrderingIndex item.ActionOrderingIndex = data.ActionOrderingIndex item.FulfillmentOrderingIndex = data.FulfillmentOrderingIndex diff --git a/pkg/code/data/fulfillment/postgres/model.go b/pkg/code/data/fulfillment/postgres/model.go index 2b3fea6b..fa564f06 100644 --- a/pkg/code/data/fulfillment/postgres/model.go +++ b/pkg/code/data/fulfillment/postgres/model.go @@ -374,7 +374,7 @@ func (m *fulfillmentModel) dbUpdate(ctx context.Context, db *sqlx.DB) error { m.VirtualBlockhash, } - if m.FulfillmentType == uint(fulfillment.CloseDormantTimelockAccount) { + if m.IntentType == uint(intent.SendPublicPayment) && m.FulfillmentType == uint(fulfillment.NoPrivacyWithdraw) { preSortingUpdateStmt = ", intent_ordering_index = $10, action_ordering_index = $11, fulfillment_ordering_index = $12" params = append( params, diff --git a/pkg/code/data/fulfillment/tests/tests.go b/pkg/code/data/fulfillment/tests/tests.go index 8da985fa..5835c05e 100644 --- a/pkg/code/data/fulfillment/tests/tests.go +++ b/pkg/code/data/fulfillment/tests/tests.go @@ -230,16 +230,16 @@ func testUpdate(t *testing.T, s fulfillment.Store) { expected := fulfillment.Record{ Intent: "test_intent", - IntentType: intent.OpenAccounts, + IntentType: intent.SendPublicPayment, ActionId: 4, - ActionType: action.CloseDormantAccount, - FulfillmentType: fulfillment.CloseDormantTimelockAccount, + ActionType: action.NoPrivacyWithdraw, + FulfillmentType: fulfillment.NoPrivacyWithdraw, Data: nil, Signature: nil, Nonce: nil, Blockhash: nil, Source: "test_source", - Destination: nil, + Destination: pointer.String("test_destination"), IntentOrderingIndex: 1, ActionOrderingIndex: 2, FulfillmentOrderingIndex: 3, diff --git a/pkg/code/data/intent/intent.go b/pkg/code/data/intent/intent.go index 32965495..ba2607ec 100644 --- a/pkg/code/data/intent/intent.go +++ b/pkg/code/data/intent/intent.go @@ -83,21 +83,17 @@ type SendPublicPaymentMetadata struct { UsdMarketValue float64 IsWithdrawal bool + IsRemoteSend bool } type ReceivePaymentsPubliclyMetadata struct { - Source string - Quantity uint64 + Source string + Quantity uint64 + IsRemoteSend bool IsReturned bool IsIssuerVoidingGiftCard bool - // Because remote send history isn't directly linked to the send. It's ok, because - // we'd expect a single payment per gift card ok (unlike temporary incoming that - // can receive many times and this metadata would be much more harder for the private - // receive). - // - // todo: A better approach? OriginalExchangeCurrency currency.Code OriginalExchangeRate float64 OriginalNativeAmount float64 @@ -293,6 +289,7 @@ func (m *SendPublicPaymentMetadata) Clone() SendPublicPaymentMetadata { NativeAmount: m.NativeAmount, UsdMarketValue: m.UsdMarketValue, IsWithdrawal: m.IsWithdrawal, + IsRemoteSend: m.IsRemoteSend, } } @@ -305,7 +302,9 @@ func (m *SendPublicPaymentMetadata) CopyTo(dst *SendPublicPaymentMetadata) { dst.ExchangeRate = m.ExchangeRate dst.NativeAmount = m.NativeAmount dst.UsdMarketValue = m.UsdMarketValue + dst.IsWithdrawal = m.IsWithdrawal + dst.IsRemoteSend = m.IsRemoteSend } func (m *SendPublicPaymentMetadata) Validate() error { @@ -338,8 +337,9 @@ func (m *SendPublicPaymentMetadata) Validate() error { func (m *ReceivePaymentsPubliclyMetadata) Clone() ReceivePaymentsPubliclyMetadata { return ReceivePaymentsPubliclyMetadata{ - Source: m.Source, - Quantity: m.Quantity, + Source: m.Source, + Quantity: m.Quantity, + IsRemoteSend: m.IsRemoteSend, IsReturned: m.IsReturned, IsIssuerVoidingGiftCard: m.IsIssuerVoidingGiftCard, @@ -355,6 +355,7 @@ func (m *ReceivePaymentsPubliclyMetadata) Clone() ReceivePaymentsPubliclyMetadat func (m *ReceivePaymentsPubliclyMetadata) CopyTo(dst *ReceivePaymentsPubliclyMetadata) { dst.Source = m.Source dst.Quantity = m.Quantity + dst.IsRemoteSend = m.IsRemoteSend dst.IsReturned = m.IsReturned dst.IsIssuerVoidingGiftCard = m.IsIssuerVoidingGiftCard diff --git a/pkg/code/data/intent/memory/store.go b/pkg/code/data/intent/memory/store.go index 4a20bbba..12485215 100644 --- a/pkg/code/data/intent/memory/store.go +++ b/pkg/code/data/intent/memory/store.go @@ -2,7 +2,6 @@ package memory import ( "context" - "errors" "sort" "sync" "time" @@ -207,6 +206,10 @@ func (s *store) filterByRemoteSendFlag(items []*intent.Record, want bool) []*int var res []*intent.Record for _, item := range items { switch item.IntentType { + case intent.SendPublicPayment: + if item.SendPublicPaymentMetadata.IsRemoteSend == want { + res = append(res, item) + } case intent.ReceivePaymentsPublicly: if item.ReceivePaymentsPubliclyMetadata.IsRemoteSend == want { res = append(res, item) @@ -290,51 +293,43 @@ func (s *store) GetLatestByInitiatorAndType(ctx context.Context, intentType inte } func (s *store) GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { - return nil, errors.New("not implemented") - - /* - s.mu.Lock() - defer s.mu.Unlock() + s.mu.Lock() + defer s.mu.Unlock() - items := s.findByDestination(giftCardVault) - items = s.filterByType(items, intent.SendPrivatePayment) - items = s.filterByState(items, false, intent.StateRevoked) - items = s.filterByRemoteSendFlag(items, true) + items := s.findByDestination(giftCardVault) + items = s.filterByType(items, intent.SendPublicPayment) + items = s.filterByState(items, false, intent.StateRevoked) + items = s.filterByRemoteSendFlag(items, true) - if len(items) == 0 { - return nil, intent.ErrIntentNotFound - } + if len(items) == 0 { + return nil, intent.ErrIntentNotFound + } - if len(items) > 1 { - return nil, intent.ErrMultilpeIntentsFound - } + if len(items) > 1 { + return nil, intent.ErrMultilpeIntentsFound + } - cloned := items[0].Clone() - return &cloned, nil - */ + cloned := items[0].Clone() + return &cloned, nil } func (s *store) GetGiftCardClaimedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { - return nil, errors.New("not implemented") - - /* - s.mu.Lock() - defer s.mu.Unlock() + s.mu.Lock() + defer s.mu.Unlock() - items := s.findBySource(giftCardVault) - items = s.filterByType(items, intent.ReceivePaymentsPublicly) - items = s.filterByState(items, false, intent.StateRevoked) - items = s.filterByRemoteSendFlag(items, true) + items := s.findBySource(giftCardVault) + items = s.filterByType(items, intent.ReceivePaymentsPublicly) + items = s.filterByState(items, false, intent.StateRevoked) + items = s.filterByRemoteSendFlag(items, true) - if len(items) == 0 { - return nil, intent.ErrIntentNotFound - } + if len(items) == 0 { + return nil, intent.ErrIntentNotFound + } - if len(items) > 1 { - return nil, intent.ErrMultilpeIntentsFound - } + if len(items) > 1 { + return nil, intent.ErrMultilpeIntentsFound + } - cloned := items[0].Clone() - return &cloned, nil - */ + cloned := items[0].Clone() + return &cloned, nil } diff --git a/pkg/code/data/intent/postgres/model.go b/pkg/code/data/intent/postgres/model.go index af9f366c..b43fb3b2 100644 --- a/pkg/code/data/intent/postgres/model.go +++ b/pkg/code/data/intent/postgres/model.go @@ -82,9 +82,11 @@ func toIntentModel(obj *intent.Record) (*intentModel, error) { m.UsdMarketValue = obj.SendPublicPaymentMetadata.UsdMarketValue m.IsWithdrawal = obj.SendPublicPaymentMetadata.IsWithdrawal + m.IsRemoteSend = obj.SendPublicPaymentMetadata.IsRemoteSend case intent.ReceivePaymentsPublicly: m.Source = obj.ReceivePaymentsPubliclyMetadata.Source m.Quantity = obj.ReceivePaymentsPubliclyMetadata.Quantity + m.IsRemoteSend = obj.ReceivePaymentsPubliclyMetadata.IsRemoteSend m.IsReturned = obj.ReceivePaymentsPubliclyMetadata.IsReturned m.IsIssuerVoidingGiftCard = obj.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard @@ -134,11 +136,13 @@ func fromIntentModel(obj *intentModel) *intent.Record { UsdMarketValue: obj.UsdMarketValue, IsWithdrawal: obj.IsWithdrawal, + IsRemoteSend: obj.IsRemoteSend, } case intent.ReceivePaymentsPublicly: record.ReceivePaymentsPubliclyMetadata = &intent.ReceivePaymentsPubliclyMetadata{ - Source: obj.Source, - Quantity: obj.Quantity, + Source: obj.Source, + Quantity: obj.Quantity, + IsRemoteSend: obj.IsRemoteSend, IsReturned: obj.IsReturned, IsIssuerVoidingGiftCard: obj.IsIssuerVoidingGiftCard, @@ -265,7 +269,7 @@ func dbGetOriginalGiftCardIssuedIntent(ctx context.Context, db *sqlx.DB, giftCar &res, query, giftCardVault, - intent.SendPrivatePayment, + intent.SendPublicPayment, intent.StateRevoked, ) if err != nil { diff --git a/pkg/code/data/intent/postgres/store.go b/pkg/code/data/intent/postgres/store.go index 12b1f3cb..5119f70a 100644 --- a/pkg/code/data/intent/postgres/store.go +++ b/pkg/code/data/intent/postgres/store.go @@ -3,7 +3,6 @@ package postgres import ( "context" "database/sql" - "errors" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/database/query" @@ -82,29 +81,21 @@ func (s *store) GetLatestByInitiatorAndType(ctx context.Context, intentType inte // GetOriginalGiftCardIssuedIntent gets the original intent where a gift card // was issued by its vault address. func (s *store) GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { - return nil, errors.New("not implemented") - - /* - model, err := dbGetOriginalGiftCardIssuedIntent(ctx, s.db, giftCardVault) - if err != nil { - return nil, err - } + model, err := dbGetOriginalGiftCardIssuedIntent(ctx, s.db, giftCardVault) + if err != nil { + return nil, err + } - return fromIntentModel(model), nil - */ + return fromIntentModel(model), nil } // GetGiftCardClaimedIntent gets the intent where a gift card was claimed by its // vault address func (s *store) GetGiftCardClaimedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { - return nil, errors.New("not implemented") - - /* - model, err := dbGetGiftCardClaimedIntent(ctx, s.db, giftCardVault) - if err != nil { - return nil, err - } + model, err := dbGetGiftCardClaimedIntent(ctx, s.db, giftCardVault) + if err != nil { + return nil, err + } - return fromIntentModel(model), nil - */ + return fromIntentModel(model), nil } diff --git a/pkg/code/data/intent/tests/tests.go b/pkg/code/data/intent/tests/tests.go index 35446c9c..533f5808 100644 --- a/pkg/code/data/intent/tests/tests.go +++ b/pkg/code/data/intent/tests/tests.go @@ -129,7 +129,9 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { ExchangeRate: 0.00073, NativeAmount: 0.00073 * 12345, UsdMarketValue: 0.00042, - IsWithdrawal: true, + + IsWithdrawal: true, + IsRemoteSend: true, }, ExtendedMetadata: []byte("extended_metadata"), State: intent.StateUnknown, @@ -153,6 +155,7 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, cloned.SendPublicPaymentMetadata.NativeAmount, actual.SendPublicPaymentMetadata.NativeAmount) assert.Equal(t, cloned.SendPublicPaymentMetadata.UsdMarketValue, actual.SendPublicPaymentMetadata.UsdMarketValue) assert.Equal(t, cloned.SendPublicPaymentMetadata.IsWithdrawal, actual.SendPublicPaymentMetadata.IsWithdrawal) + assert.Equal(t, cloned.SendPublicPaymentMetadata.IsRemoteSend, actual.SendPublicPaymentMetadata.IsRemoteSend) assert.Equal(t, cloned.ExtendedMetadata, actual.ExtendedMetadata) assert.Equal(t, cloned.State, actual.State) assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) @@ -271,89 +274,81 @@ func testGetLatestByInitiatorAndType(t *testing.T, s intent.Store) { func testGetOriginalGiftCardIssuedIntent(t *testing.T, s intent.Store) { t.Run("testGetOriginalGiftCardIssuedIntent", func(t *testing.T) { - /* - ctx := context.Background() + ctx := context.Background() - records := []intent.Record{ - {IntentId: "i1", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a1", DestinationOwnerAccount: "o1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + records := []intent.Record{ + {IntentId: "i1", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a1", DestinationOwnerAccount: "o1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i2", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i3", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i4", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i5", IntentType: intent.ExternalDeposit, ExternalDepositMetadata: &intent.ExternalDepositMetadata{DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i6", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "source", Destination: "a2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i2", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i3", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i4", IntentType: intent.ExternalDeposit, ExternalDepositMetadata: &intent.ExternalDepositMetadata{DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i7", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i8", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i5", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i6", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i9", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, - {IntentId: "i10", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, - } + {IntentId: "i7", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, + {IntentId: "i8", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, + } - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } + for _, record := range records { + require.NoError(t, s.Save(ctx, &record)) + } - _, err := s.GetOriginalGiftCardIssuedIntent(ctx, "unknown") - assert.Equal(t, intent.ErrIntentNotFound, err) + _, err := s.GetOriginalGiftCardIssuedIntent(ctx, "unknown") + assert.Equal(t, intent.ErrIntentNotFound, err) - _, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a1") - assert.Equal(t, intent.ErrIntentNotFound, err) + _, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a1") + assert.Equal(t, intent.ErrIntentNotFound, err) - actual, err := s.GetOriginalGiftCardIssuedIntent(ctx, "a2") - require.NoError(t, err) - assert.Equal(t, "i2", actual.IntentId) + actual, err := s.GetOriginalGiftCardIssuedIntent(ctx, "a2") + require.NoError(t, err) + assert.Equal(t, "i2", actual.IntentId) - _, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a3") - assert.Equal(t, intent.ErrMultilpeIntentsFound, err) + _, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a3") + assert.Equal(t, intent.ErrMultilpeIntentsFound, err) - actual, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a4") - require.NoError(t, err) - assert.Equal(t, "i9", actual.IntentId) - */ + actual, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a4") + require.NoError(t, err) + assert.Equal(t, "i7", actual.IntentId) }) } func testGetGiftCardClaimedIntent(t *testing.T, s intent.Store) { t.Run("testGetGiftCardClaimedIntent", func(t *testing.T) { - /* - ctx := context.Background() + ctx := context.Background() - records := []intent.Record{ - {IntentId: "i1", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a1", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + records := []intent.Record{ + {IntentId: "i1", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a1", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i2", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i3", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i4", IntentType: intent.ReceivePaymentsPrivately, ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{Source: "a2", Quantity: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i5", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a2", Destination: "destination", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i2", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i3", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i6", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i7", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i4", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i5", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i8", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, - {IntentId: "i9", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, - } + {IntentId: "i6", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, + {IntentId: "i7", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, + } - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } + for _, record := range records { + require.NoError(t, s.Save(ctx, &record)) + } - _, err := s.GetGiftCardClaimedIntent(ctx, "unknown") - assert.Equal(t, intent.ErrIntentNotFound, err) + _, err := s.GetGiftCardClaimedIntent(ctx, "unknown") + assert.Equal(t, intent.ErrIntentNotFound, err) - _, err = s.GetGiftCardClaimedIntent(ctx, "a1") - assert.Equal(t, intent.ErrIntentNotFound, err) + _, err = s.GetGiftCardClaimedIntent(ctx, "a1") + assert.Equal(t, intent.ErrIntentNotFound, err) - actual, err := s.GetGiftCardClaimedIntent(ctx, "a2") - require.NoError(t, err) - assert.Equal(t, "i3", actual.IntentId) + actual, err := s.GetGiftCardClaimedIntent(ctx, "a2") + require.NoError(t, err) + assert.Equal(t, "i3", actual.IntentId) - _, err = s.GetGiftCardClaimedIntent(ctx, "a3") - assert.Equal(t, intent.ErrMultilpeIntentsFound, err) + _, err = s.GetGiftCardClaimedIntent(ctx, "a3") + assert.Equal(t, intent.ErrMultilpeIntentsFound, err) - actual, err = s.GetGiftCardClaimedIntent(ctx, "a4") - require.NoError(t, err) - assert.Equal(t, "i9", actual.IntentId) - */ + actual, err = s.GetGiftCardClaimedIntent(ctx, "a4") + require.NoError(t, err) + assert.Equal(t, "i7", actual.IntentId) }) } diff --git a/pkg/code/server/account/server.go b/pkg/code/server/account/server.go index e77bebed..5d88014e 100644 --- a/pkg/code/server/account/server.go +++ b/pkg/code/server/account/server.go @@ -2,6 +2,7 @@ package account import ( "context" + "errors" "time" "github.com/sirupsen/logrus" @@ -420,11 +421,6 @@ func (s *server) getProtoAccountInfo(ctx context.Context, records *common.Accoun blockchainState = accountpb.TokenAccountInfo_BLOCKCHAIN_STATE_UNKNOWN } - mustRotate, err := s.shouldClientRotateAccount(ctx, records, prefetchedBalanceMetadata.value) - if err != nil { - return nil, err - } - // Claimed states only apply to gift card accounts var claimState accountpb.TokenAccountInfo_ClaimState if records.General.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { @@ -476,14 +472,12 @@ func (s *server) getProtoAccountInfo(ctx context.Context, records *common.Accoun } var originalExchangeData *transactionpb.ExchangeData - /* - if records.General.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - originalExchangeData, err = s.getOriginalGiftCardExchangeData(ctx, records) - if err != nil { - return nil, err - } + if records.General.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { + originalExchangeData, err = s.getOriginalGiftCardExchangeData(ctx, records) + if err != nil { + return nil, err } - */ + } return &accountpb.TokenAccountInfo{ Address: tokenAccount.ToProto(), @@ -495,7 +489,6 @@ func (s *server) getProtoAccountInfo(ctx context.Context, records *common.Accoun Balance: prefetchedBalanceMetadata.value, ManagementState: managementState, BlockchainState: blockchainState, - MustRotate: mustRotate, ClaimState: claimState, OriginalExchangeData: originalExchangeData, Mint: mintAccount.ToProto(), @@ -503,17 +496,6 @@ func (s *server) getProtoAccountInfo(ctx context.Context, records *common.Accoun }, nil } -func (s *server) shouldClientRotateAccount(ctx context.Context, records *common.AccountRecords, balance uint64) (bool, error) { - // Only temp incoming accounts require server hints to rotate - if records.General.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { - return false, nil - } - - // Rotation should occur if the account has a balance - return balance > 0, nil -} - -/* func (s *server) getOriginalGiftCardExchangeData(ctx context.Context, records *common.AccountRecords) (*transactionpb.ExchangeData, error) { if records.General.AccountType != commonpb.AccountType_REMOTE_SEND_GIFT_CARD { return nil, errors.New("invalid account type") @@ -525,10 +507,9 @@ func (s *server) getOriginalGiftCardExchangeData(ctx context.Context, records *c } return &transactionpb.ExchangeData{ - Currency: string(intentRecord.SendPrivatePaymentMetadata.ExchangeCurrency), - ExchangeRate: intentRecord.SendPrivatePaymentMetadata.ExchangeRate, - NativeAmount: intentRecord.SendPrivatePaymentMetadata.NativeAmount, - Quarks: intentRecord.SendPrivatePaymentMetadata.Quantity, + Currency: string(intentRecord.SendPublicPaymentMetadata.ExchangeCurrency), + ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate, + NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount, + Quarks: intentRecord.SendPublicPaymentMetadata.Quantity, }, nil } -*/ diff --git a/pkg/code/server/account/server_test.go b/pkg/code/server/account/server_test.go index effbeb60..947fc482 100644 --- a/pkg/code/server/account/server_test.go +++ b/pkg/code/server/account/server_test.go @@ -6,7 +6,6 @@ import ( "fmt" "math/rand" "testing" - "time" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" @@ -25,6 +24,8 @@ import ( "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/code/data/transaction" + "github.com/code-payments/code-server/pkg/currency" + "github.com/code-payments/code-server/pkg/pointer" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/testutil" ) @@ -225,7 +226,6 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { assert.Equal(t, common.CoreMintAccount.PublicKey().ToBytes(), accountInfo.Mint.Value) } - assert.False(t, accountInfo.MustRotate) assert.Equal(t, accountpb.TokenAccountInfo_CLAIM_STATE_UNKNOWN, accountInfo.ClaimState) assert.Nil(t, accountInfo.OriginalExchangeData) } @@ -235,7 +235,6 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { assert.True(t, primaryAccountInfoRecord.RequiresDepositSync) } -/* func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { env, cleanup := setup(t) defer cleanup() @@ -381,11 +380,11 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { giftCardIssuedIntentRecord := &intent.Record{ IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.SendPrivatePayment, + IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: testutil.NewRandomAccount(t).PrivateKey().ToBase58(), - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ + SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ DestinationTokenAccount: accountRecords.General.TokenAccount, Quantity: common.ToCoreMintQuarks(10), @@ -408,10 +407,10 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { autoReturnActionRecord := &action.Record{ Intent: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.SendPrivatePayment, + IntentType: intent.SendPublicPayment, ActionId: 0, - ActionType: action.CloseDormantAccount, + ActionType: action.NoPrivacyWithdraw, Source: accountRecords.General.TokenAccount, Destination: pointer.String("primary"), @@ -474,19 +473,16 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { assert.Equal(t, tc.expectedClaimState, accountInfo.ClaimState) require.NotNil(t, accountInfo.OriginalExchangeData) - assert.EqualValues(t, giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeCurrency, accountInfo.OriginalExchangeData.Currency) - assert.Equal(t, giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeRate, accountInfo.OriginalExchangeData.ExchangeRate) - assert.Equal(t, giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.NativeAmount, accountInfo.OriginalExchangeData.NativeAmount) - assert.Equal(t, giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.Quantity, accountInfo.OriginalExchangeData.Quarks) - - assert.False(t, accountInfo.MustRotate) + assert.EqualValues(t, giftCardIssuedIntentRecord.SendPublicPaymentMetadata.ExchangeCurrency, accountInfo.OriginalExchangeData.Currency) + assert.Equal(t, giftCardIssuedIntentRecord.SendPublicPaymentMetadata.ExchangeRate, accountInfo.OriginalExchangeData.ExchangeRate) + assert.Equal(t, giftCardIssuedIntentRecord.SendPublicPaymentMetadata.NativeAmount, accountInfo.OriginalExchangeData.NativeAmount) + assert.Equal(t, giftCardIssuedIntentRecord.SendPublicPaymentMetadata.Quantity, accountInfo.OriginalExchangeData.Quarks) accountInfoRecord, err := env.data.GetLatestAccountInfoByOwnerAddressAndType(env.ctx, ownerAccount.PublicKey().ToBase58(), commonpb.AccountType_REMOTE_SEND_GIFT_CARD) require.NoError(t, err) assert.False(t, accountInfoRecord.RequiresDepositSync) } } -*/ func TestGetTokenAccountInfos_BlockchainState(t *testing.T) { env, cleanup := setup(t) @@ -596,58 +592,6 @@ func TestGetTokenAccountInfos_ManagementState(t *testing.T) { } } -func TestGetTokenAccountInfos_TempIncomingAccountRotation_AtLeastOnePayment(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - req := &accountpb.GetTokenAccountInfosRequest{ - Owner: ownerAccount.ToProto(), - } - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), - } - - tempIncomingDerivedOwner := testutil.NewRandomAccount(t) - accountRecords := setupAccountRecords(t, env, ownerAccount, tempIncomingDerivedOwner, 2, commonpb.AccountType_TEMPORARY_INCOMING) - - resp, err := env.client.GetTokenAccountInfos(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - require.Len(t, resp.TokenAccountInfos, 1) - - accountInfo, ok := resp.TokenAccountInfos[accountRecords.General.TokenAccount] - require.True(t, ok) - assert.False(t, accountInfo.MustRotate) - - for i := 0; i < 3; i++ { - quantity := uint64(1) - actionRecord := &action.Record{ - Intent: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.SendPrivatePayment, - ActionType: action.NoPrivacyWithdraw, - Source: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Destination: &accountRecords.General.TokenAccount, - Quantity: &quantity, - State: action.StatePending, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutAllActions(env.ctx, actionRecord)) - - resp, err := env.client.GetTokenAccountInfos(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - require.Len(t, resp.TokenAccountInfos, 1) - - accountInfo, ok := resp.TokenAccountInfos[accountRecords.General.TokenAccount] - require.True(t, ok) - assert.True(t, accountInfo.MustRotate) - } -} - func TestGetTokenAccountInfos_NoTokenAccounts(t *testing.T) { env, cleanup := setup(t) defer cleanup() diff --git a/pkg/code/server/transaction/action_handler.go b/pkg/code/server/transaction/action_handler.go index ad88a162..fa8f8385 100644 --- a/pkg/code/server/transaction/action_handler.go +++ b/pkg/code/server/transaction/action_handler.go @@ -3,6 +3,7 @@ package transaction_v2 import ( "context" "errors" + "math" "time" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" @@ -15,6 +16,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/timelock" + "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/cvm" ) @@ -35,8 +37,10 @@ type newFulfillmentMetadata struct { source *common.Account destination *common.Account - fulfillmentOrderingIndex uint32 - disableActiveScheduling bool + intentOrderingIndexOverride *uint64 + actionOrderingIndexOverride *uint32 + fulfillmentOrderingIndex uint32 + disableActiveScheduling bool } // BaseActionHandler is a base interface for operation-specific action handlers @@ -273,8 +277,7 @@ func (h *NoPrivacyTransferActionHandler) FulfillmentCount() int { func (h *NoPrivacyTransferActionHandler) PopulateMetadata(actionRecord *action.Record) error { actionRecord.Source = h.source.Vault.PublicKey().ToBase58() - destination := h.destination.PublicKey().ToBase58() - actionRecord.Destination = &destination + actionRecord.Destination = pointer.String(h.destination.PublicKey().ToBase58()) actionRecord.Quantity = &h.amount @@ -345,22 +348,13 @@ func (h *NoPrivacyTransferActionHandler) OnSaveToDB(ctx context.Context) error { } type NoPrivacyWithdrawActionHandler struct { - source *common.TimelockAccounts - destination *common.Account - amount uint64 - disableActiveScheduling bool + source *common.TimelockAccounts + destination *common.Account + amount uint64 + isAutoReturn bool } func NewNoPrivacyWithdrawActionHandler(intentRecord *intent.Record, protoAction *transactionpb.NoPrivacyWithdrawAction) (CreateActionHandler, error) { - var disableActiveScheduling bool - - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - // Technically we should do this for public receives too, but we don't - // yet have a great way of doing cross intent fulfillment polling hints. - disableActiveScheduling = true - } - sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) if err != nil { return nil, err @@ -377,10 +371,10 @@ func NewNoPrivacyWithdrawActionHandler(intentRecord *intent.Record, protoAction } return &NoPrivacyWithdrawActionHandler{ - source: source, - destination: destination, - amount: protoAction.Amount, - disableActiveScheduling: disableActiveScheduling, + source: source, + destination: destination, + amount: protoAction.Amount, + isAutoReturn: protoAction.IsAutoReturn, }, nil } @@ -391,13 +385,21 @@ func (h *NoPrivacyWithdrawActionHandler) FulfillmentCount() int { func (h *NoPrivacyWithdrawActionHandler) PopulateMetadata(actionRecord *action.Record) error { actionRecord.Source = h.source.Vault.PublicKey().ToBase58() - destination := h.destination.PublicKey().ToBase58() - actionRecord.Destination = &destination + actionRecord.Destination = pointer.String(h.destination.PublicKey().ToBase58()) actionRecord.Quantity = &h.amount actionRecord.State = action.StatePending + if h.isAutoReturn { + // Do not populate a quantity. This will be done later when we decide to schedule + // the action. Otherwise, the balance calculator will be completely off. Balance + // amount will be determined at time of scheduling + actionRecord.Quantity = nil + + actionRecord.State = action.StateUnknown + } + return nil } func (h *NoPrivacyWithdrawActionHandler) GetServerParameter() *transactionpb.ServerParameter { @@ -431,12 +433,14 @@ func (h *NoPrivacyWithdrawActionHandler) GetFulfillmentMetadata( expectedSigner: h.source.VaultOwner, virtualIxnHash: &virtualIxnHash, - fulfillmentType: fulfillment.NoPrivacyWithdraw, - source: h.source.Vault, - destination: h.destination, - fulfillmentOrderingIndex: 0, + fulfillmentType: fulfillment.NoPrivacyWithdraw, + source: h.source.Vault, + destination: h.destination, - disableActiveScheduling: h.disableActiveScheduling, + intentOrderingIndexOverride: pointer.Uint64(math.MaxInt64), + actionOrderingIndexOverride: pointer.Uint32(0), + fulfillmentOrderingIndex: 0, + disableActiveScheduling: h.isAutoReturn, }, nil default: return nil, errors.New("invalid transaction index") diff --git a/pkg/code/server/transaction/intent.go b/pkg/code/server/transaction/intent.go index 60e1e2b4..85b5d3e0 100644 --- a/pkg/code/server/transaction/intent.go +++ b/pkg/code/server/transaction/intent.go @@ -100,9 +100,8 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm log = log.WithField("intent_type", "send_public_payment") intentHandler = NewSendPublicPaymentIntentHandler(s.conf, s.data, s.antispamGuard) case *transactionpb.Metadata_ReceivePaymentsPublicly: - return newIntentDeniedError("remote send requires rewrite") - //log = log.WithField("intent_type", "receive_payments_publicly") - //intentHandler = NewReceivePaymentsPubliclyIntentHandler(s.conf, s.data, s.antispamGuard) + log = log.WithField("intent_type", "receive_payments_publicly") + intentHandler = NewReceivePaymentsPubliclyIntentHandler(s.conf, s.data, s.antispamGuard) default: return handleSubmitIntentError(streamer, status.Error(codes.InvalidArgument, "SubmitIntentRequest.SubmitActions.Metadata is nil")) } @@ -131,7 +130,7 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm case common.OwnerTypeRemoteSendGiftCard: // Remote send gift cards can only be the owner of an intent for a // remote send public receive. In this instance, we need to inspect - // the destination account, which should be a user's temporary incoming + // the destination account, which should be a user's primary // account. // // todo: This is a bit of a mess and should realistically be a generic @@ -145,8 +144,8 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm if err != nil && err != account.ErrAccountInfoNotFound { log.WithError(err).Warn("failure getting user initiator owner account") return handleSubmitIntentError(streamer, err) - } else if err == account.ErrAccountInfoNotFound || accountInfoRecord.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { - return newActionValidationError(submitActionsReq.Actions[0], "destination must be a temporary incoming account") + } else if err == account.ErrAccountInfoNotFound || accountInfoRecord.AccountType != commonpb.AccountType_PRIMARY { + return newActionValidationError(submitActionsReq.Actions[0], "destination must be a primary account") } initiatorOwnerAccount, err = common.NewAccountFromPublicKeyString(accountInfoRecord.OwnerAccount) @@ -339,6 +338,8 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm requiresClientSignature bool expectedSigner *common.Account virtualIxnHash *cvm.CompactMessage + + intentOrderingIndexOverriden bool } // Convert all actions into a set of fulfillments @@ -524,6 +525,12 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm if newFulfillmentMetadata.destination != nil { fulfillmentRecord.Destination = pointer.String(newFulfillmentMetadata.destination.PublicKey().ToBase58()) } + if newFulfillmentMetadata.intentOrderingIndexOverride != nil { + fulfillmentRecord.IntentOrderingIndex = *newFulfillmentMetadata.intentOrderingIndexOverride + } + if newFulfillmentMetadata.actionOrderingIndexOverride != nil { + fulfillmentRecord.ActionOrderingIndex = *newFulfillmentMetadata.actionOrderingIndexOverride + } // Fulfillment has a virtual instruction requiring client signature if newFulfillmentMetadata.requiresClientSignature { @@ -544,6 +551,8 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm requiresClientSignature: newFulfillmentMetadata.requiresClientSignature, expectedSigner: newFulfillmentMetadata.expectedSigner, virtualIxnHash: newFulfillmentMetadata.virtualIxnHash, + + intentOrderingIndexOverriden: newFulfillmentMetadata.intentOrderingIndexOverride != nil, }) reservedNonces = append(reservedNonces, selectedNonce) } @@ -663,7 +672,9 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm // Save all fulfillment records fulfillmentRecordsToSave := make([]*fulfillment.Record, 0) for i, fulfillmentWithMetadata := range fulfillments { - fulfillmentWithMetadata.record.IntentOrderingIndex = intentRecord.Id + if !fulfillmentWithMetadata.intentOrderingIndexOverriden { + fulfillmentWithMetadata.record.IntentOrderingIndex = intentRecord.Id + } fulfillmentRecordsToSave = append(fulfillmentRecordsToSave, fulfillmentWithMetadata.record) @@ -1003,10 +1014,9 @@ func (s *transactionServer) CanWithdrawToAccount(ctx context.Context, req *trans if timelockRecord != nil { accountInfoRecord, err := s.data.GetAccountInfoByTokenAddress(ctx, accountToCheck.PublicKey().ToBase58()) if err == nil { - // todo: may need to check if we're going to close the primary account when supported in the future return &transactionpb.CanWithdrawToAccountResponse{ AccountType: transactionpb.CanWithdrawToAccountResponse_TokenAccount, - IsValidPaymentDestination: accountInfoRecord.AccountType == commonpb.AccountType_PRIMARY || accountInfoRecord.AccountType == commonpb.AccountType_RELATIONSHIP, + IsValidPaymentDestination: accountInfoRecord.AccountType == commonpb.AccountType_PRIMARY, }, nil } else { log.WithError(err).Warn("failure checking account info db") diff --git a/pkg/code/server/transaction/intent_handler.go b/pkg/code/server/transaction/intent_handler.go index 581239fd..f2d27231 100644 --- a/pkg/code/server/transaction/intent_handler.go +++ b/pkg/code/server/transaction/intent_handler.go @@ -315,6 +315,10 @@ func (h *SendPublicPaymentIntentHandler) PopulateMetadata(ctx context.Context, i intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount = destinationAccountInfo.OwnerAccount } + if intentRecord.SendPublicPaymentMetadata.IsRemoteSend && intentRecord.SendPublicPaymentMetadata.IsWithdrawal { + return newIntentValidationError("remote send cannot be a withdraw") + } + return nil } @@ -433,28 +437,27 @@ func (h *SendPublicPaymentIntentHandler) AllowCreation(ctx context.Context, inte initiatiorOwnerAccount, initiatorAccountsByType, initiatorAccountsByVault, - intentRecord, typedMetadata, actions, simResult, ) } +// todo: For remote send, we still need to fully validate the auto-return action func (h *SendPublicPaymentIntentHandler) validateActions( ctx context.Context, initiatorOwnerAccount *common.Account, initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, initiatorAccountsByVault map[string]*common.AccountRecords, - intentRecord *intent.Record, metadata *transactionpb.SendPublicPaymentMetadata, actions []*transactionpb.Action, simResult *LocalSimulationResult, ) error { - if len(actions) != 1 { + if !metadata.IsRemoteSend && len(actions) != 1 { return newIntentValidationError("expected 1 action") } - if metadata.IsRemoteSend { - return newIntentDeniedError("remote send is not implemented") + if metadata.IsRemoteSend && len(actions) != 3 { + return newIntentValidationError("expected 3 actions") } var source *common.Account @@ -483,6 +486,11 @@ func (h *SendPublicPaymentIntentHandler) validateActions( destinationAccountInfo, err := h.data.GetAccountInfoByTokenAddress(ctx, destination.PublicKey().ToBase58()) switch err { case nil: + // Remote sends must be to a brand new gift card account + if metadata.IsRemoteSend { + return newIntentValidationError("destination must be a brand new gift card account") + } + // Code->Code public withdraws must be done against other deposit accounts if metadata.IsWithdrawal && destinationAccountInfo.AccountType != commonpb.AccountType_PRIMARY { return newIntentValidationError("destination account must be a deposit account") @@ -493,9 +501,10 @@ func (h *SendPublicPaymentIntentHandler) validateActions( return newIntentValidationError("payment is a no-op") } case account.ErrAccountInfoNotFound: - // Check whether the destination account is a Kin token account that's - // been created on the blockchain. - if !h.conf.disableBlockchainChecks.Get(ctx) { + // Check whether the destination account is a core mint token account that's + // been created on the blockchain. Exception is made when we're doing a remote + // send, since we expect the gift card account to no yet exist. + if !metadata.IsRemoteSend && !h.conf.disableBlockchainChecks.Get(ctx) { err = validateExternalTokenAccountWithinIntent(ctx, h.data, destination) if err != nil { return err @@ -523,7 +532,7 @@ func (h *SendPublicPaymentIntentHandler) validateActions( return newIntentValidationErrorf("must send payment to destination account %s", destination.PublicKey().ToBase58()) } else if destinationSimulation.Transfers[0].IsPrivate || destinationSimulation.Transfers[0].IsWithdraw { return newActionValidationError(destinationSimulation.Transfers[0].Action, "payment sent to destination must be a public transfer") - } else if destinationSimulation.GetDeltaQuarks() != int64(metadata.ExchangeData.Quarks) { + } else if destinationSimulation.GetDeltaQuarks(false) != int64(metadata.ExchangeData.Quarks) { return newActionValidationErrorf(destinationSimulation.Transfers[0].Action, "must send %d quarks to destination account", metadata.ExchangeData.Quarks) } @@ -535,7 +544,7 @@ func (h *SendPublicPaymentIntentHandler) validateActions( sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] if !ok { return newIntentValidationErrorf("must send payment from source account %s", source.PublicKey().ToBase58()) - } else if sourceSimulation.GetDeltaQuarks() != -int64(metadata.ExchangeData.Quarks) { + } else if sourceSimulation.GetDeltaQuarks(false) != -int64(metadata.ExchangeData.Quarks) { return newActionValidationErrorf(sourceSimulation.Transfers[0].Action, "must send %d quarks from source account", metadata.ExchangeData.Quarks) } @@ -546,14 +555,53 @@ func (h *SendPublicPaymentIntentHandler) validateActions( return err } - // Part 4: Sanity check no open and closed accounts + // Part 4: Validate open and closed accounts - if len(simResult.GetOpenedAccounts()) > 0 { - return newIntentValidationError("cannot open any account") - } + if metadata.IsRemoteSend { + if len(simResult.GetOpenedAccounts()) != 1 { + return newIntentValidationError("expected 1 account opened") + } + + err = validateGiftCardAccountOpened( + ctx, + h.data, + initiatorOwnerAccount, + destination, + actions, + ) + if err != nil { + return err + } + + closedAccounts := simResult.GetClosedAccounts() + if len(FilterAutoReturnedAccounts(closedAccounts)) != 0 { + return newIntentValidationError("expected no closed accounts outside of auto-returns") + } + if len(closedAccounts) != 1 { + return newIntentValidationError("expected exactly 1 auto-returned account") + } - if len(simResult.GetClosedAccounts()) > 0 { - return newIntentValidationError("cannot close any account") + autoReturns := destinationSimulation.GetAutoReturns() + if len(autoReturns) != 1 { + return newIntentValidationError("expected auto-return for the remote send gift card") + } else if autoReturns[0].IsPrivate || !autoReturns[0].IsWithdraw { + return newActionValidationError(destinationSimulation.Transfers[0].Action, "auto-return must be a public withdraw") + } else if autoReturns[0].DeltaQuarks != -int64(metadata.ExchangeData.Quarks) { + return newActionValidationErrorf(autoReturns[0].Action, "must auto-return %d quarks from remote send gift card", metadata.ExchangeData.Quarks) + } + + autoReturns = sourceSimulation.GetAutoReturns() + if len(autoReturns) != 1 { + return newIntentValidationError("gift card auto-return balance must go to the source account") + } + } else { + if len(simResult.GetOpenedAccounts()) > 0 { + return newIntentValidationError("cannot open any account") + } + + if len(simResult.GetClosedAccounts()) > 0 { + return newIntentValidationError("cannot close any account") + } } return nil @@ -585,56 +633,53 @@ func NewReceivePaymentsPubliclyIntentHandler(conf *conf, data code_data.Provider } func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { - return newIntentDeniedError("remote send requires rewrite") + typedProtoMetadata := protoMetadata.GetReceivePaymentsPublicly() + if typedProtoMetadata == nil { + return errors.New("unexpected metadata proto message") + } - /* - typedProtoMetadata := protoMetadata.GetReceivePaymentsPublicly() - if typedProtoMetadata == nil { - return errors.New("unexpected metadata proto message") - } + giftCardVault, err := common.NewAccountFromPublicKeyBytes(typedProtoMetadata.Source.Value) + if err != nil { + return err + } - giftCardVault, err := common.NewAccountFromPublicKeyBytes(typedProtoMetadata.Source.Value) - if err != nil { - return err - } + usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) + if err != nil { + return errors.Wrap(err, "error getting current usd exchange rate") + } - usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) - if err != nil { - return errors.Wrap(err, "error getting current usd exchange rate") - } + // This is an optimization for payment history. Original fiat amounts are not + // easily linked due to the nature of gift cards and the remote send flow. We + // fetch this metadata up front so we don't need to do it every time in history. + giftCardIssuedIntentRecord, err := h.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVault.PublicKey().ToBase58()) + if err == intent.ErrIntentNotFound { + return newIntentValidationError("source is not a remote send gift card") + } else if err != nil { + return err + } + h.cachedGiftCardIssuedIntentRecord = giftCardIssuedIntentRecord - // This is an optimization for payment history. Original fiat amounts are not - // easily linked due to the nature of gift cards and the remote send flow. We - // fetch this metadata up front so we don't need to do it every time in history. - giftCardIssuedIntentRecord, err := h.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVault.PublicKey().ToBase58()) - if err == intent.ErrIntentNotFound { - return newIntentValidationError("source is not a remote send gift card") - } else if err != nil { - return err - } - h.cachedGiftCardIssuedIntentRecord = giftCardIssuedIntentRecord + intentRecord.IntentType = intent.ReceivePaymentsPublicly + intentRecord.ReceivePaymentsPubliclyMetadata = &intent.ReceivePaymentsPubliclyMetadata{ + Source: giftCardVault.PublicKey().ToBase58(), + Quantity: typedProtoMetadata.Quarks, - intentRecord.IntentType = intent.ReceivePaymentsPublicly - intentRecord.ReceivePaymentsPubliclyMetadata = &intent.ReceivePaymentsPubliclyMetadata{ - Source: giftCardVault.PublicKey().ToBase58(), - Quantity: typedProtoMetadata.Quarks, - IsRemoteSend: typedProtoMetadata.IsRemoteSend, - IsReturned: false, - IsIssuerVoidingGiftCard: typedProtoMetadata.IsIssuerVoidingGiftCard, + IsRemoteSend: typedProtoMetadata.IsRemoteSend, + IsReturned: false, + IsIssuerVoidingGiftCard: typedProtoMetadata.IsIssuerVoidingGiftCard, - OriginalExchangeCurrency: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeCurrency, - OriginalExchangeRate: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeRate, - OriginalNativeAmount: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.NativeAmount, + OriginalExchangeCurrency: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.ExchangeCurrency, + OriginalExchangeRate: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.ExchangeRate, + OriginalNativeAmount: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.NativeAmount, - UsdMarketValue: usdExchangeRecord.Rate * float64(typedProtoMetadata.Quarks) / float64(common.CoreMintQuarksPerUnit), - } + UsdMarketValue: usdExchangeRecord.Rate * float64(typedProtoMetadata.Quarks) / float64(common.CoreMintQuarksPerUnit), + } - if intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard && intentRecord.InitiatorOwnerAccount != giftCardIssuedIntentRecord.InitiatorOwnerAccount { - return newIntentValidationError("only the issuer can void the gift card") - } + if intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard && intentRecord.InitiatorOwnerAccount != giftCardIssuedIntentRecord.InitiatorOwnerAccount { + return newIntentValidationError("only the issuer can void the gift card") + } - return nil - */ + return nil } func (h *ReceivePaymentsPubliclyIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { @@ -756,8 +801,6 @@ func (h *ReceivePaymentsPubliclyIntentHandler) AllowCreation(ctx context.Context // return h.validateActions( - ctx, - initiatiorOwnerAccount, initiatorAccountsByType, initiatorAccountsByVault, typedMetadata, @@ -767,8 +810,6 @@ func (h *ReceivePaymentsPubliclyIntentHandler) AllowCreation(ctx context.Context } func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( - ctx context.Context, - initiatorOwnerAccount *common.Account, initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, initiatorAccountsByVault map[string]*common.AccountRecords, metadata *transactionpb.ReceivePaymentsPubliclyMetadata, @@ -789,17 +830,11 @@ func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( return err } - // The destination account must be the latest temporary incoming account - destinationAccountInfo := initiatorAccountsByType[commonpb.AccountType_TEMPORARY_INCOMING][0].General + // The destination account must be the primary + destinationAccountInfo := initiatorAccountsByType[commonpb.AccountType_PRIMARY][0].General destinationSimulation, ok := simResult.SimulationsByAccount[destinationAccountInfo.TokenAccount] if !ok { - return newActionValidationError(actions[0], "must send payment to latest temp incoming account") - } - - // And that temporary incoming account has limited usage - err = validateMinimalTempIncomingAccountUsage(ctx, h.data, destinationAccountInfo) - if err != nil { - return err + return newActionValidationError(actions[0], "must send payment to primary account") } // @@ -813,7 +848,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] if !ok { return newIntentValidationError("must receive payments from source account") - } else if sourceSimulation.GetDeltaQuarks() != -int64(metadata.Quarks) { + } else if sourceSimulation.GetDeltaQuarks(false) != -int64(metadata.Quarks) { return newActionValidationErrorf(sourceSimulation.Transfers[0].Action, "must receive %d quarks from source account", metadata.Quarks) } else if sourceSimulation.Transfers[0].IsPrivate || !sourceSimulation.Transfers[0].IsWithdraw { return newActionValidationError(sourceSimulation.Transfers[0].Action, "transfer must be a public withdraw") @@ -823,7 +858,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( // Part 2.2: Check destination account is paid exact quark amount from source account in a public withdraw // - if destinationSimulation.GetDeltaQuarks() != int64(metadata.Quarks) { + if destinationSimulation.GetDeltaQuarks(false) != int64(metadata.Quarks) { return newActionValidationErrorf(actions[0], "must receive %d quarks to temp incoming account", metadata.Quarks) } else if destinationSimulation.Transfers[0].IsPrivate || !destinationSimulation.Transfers[0].IsWithdraw { return newActionValidationError(sourceSimulation.Transfers[0].Action, "transfer must be a public withdraw") @@ -870,26 +905,6 @@ func validateAllUserAccountsManagedByCode(ctx context.Context, initiatorAccounts return nil } -func validateMoneyMovementActionCount(actions []*transactionpb.Action) error { - var numMoneyMovementActions int - - for _, action := range actions { - switch action.Type.(type) { - case *transactionpb.Action_NoPrivacyWithdraw, - *transactionpb.Action_NoPrivacyTransfer, - *transactionpb.Action_FeePayment: - - numMoneyMovementActions++ - } - } - - // todo: configurable - if numMoneyMovementActions > 50 { - return newIntentDeniedError("too many transfer/exchange/withdraw actions") - } - return nil -} - // Provides generic and lightweight validation of which accounts owned by a Code // user can be used in certain actions. This is by no means a comprehensive check. // Other account types (eg. gift cards, external wallets, etc) and intent-specific @@ -923,8 +938,8 @@ func validateMoneyMovementActionUserAccounts( } case *transactionpb.Action_NoPrivacyWithdraw: // No privacy withdraws are used in two ways depending on the intent: - // 1. As the sender of funds from the latest temp outgoing account in a private send - // 2. As a receiver of funds to the latest temp incoming account in a public receive + // 1. As an auto-return action back to the payer's primary account in a public payment intent for remote send + // 2. As a receiver of funds to the primary account in a public receive authority, err = common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Authority) if err != nil { @@ -942,14 +957,14 @@ func validateMoneyMovementActionUserAccounts( } switch intentType { - case intent.ReceivePaymentsPublicly: + case intent.SendPublicPayment, intent.ReceivePaymentsPublicly: destinationAccountInfo, ok := initiatorAccountsByVault[destination.PublicKey().ToBase58()] - if !ok || destinationAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { - return newActionValidationError(action, "source account must be the latest temporary incoming account") + if !ok || destinationAccountInfo.General.AccountType != commonpb.AccountType_PRIMARY { + return newActionValidationError(action, "source account must be the primary account") } } case *transactionpb.Action_FeePayment: - // Fee payments always come from the latest temporary outgoing account + // Fee payments always come from the primary account authority, err = common.NewAccountFromProto(typedAction.FeePayment.Authority) if err != nil { @@ -962,8 +977,8 @@ func validateMoneyMovementActionUserAccounts( } sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] - if !ok || sourceAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_OUTGOING { - return newActionValidationError(action, "source account must be the latest temporary outgoing account") + if !ok || sourceAccountInfo.General.AccountType != commonpb.AccountType_PRIMARY { + return newActionValidationError(action, "source account must be the primary account") } default: continue @@ -980,72 +995,11 @@ func validateMoneyMovementActionUserAccounts( return nil } -func validateNextTemporaryAccountOpened( - accountType commonpb.AccountType, - initiatorOwnerAccount *common.Account, - initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, - actions []*transactionpb.Action, -) error { - if accountType != commonpb.AccountType_TEMPORARY_INCOMING && accountType != commonpb.AccountType_TEMPORARY_OUTGOING { - return errors.New("unexpected account type") - } - - prevTempAccountRecords, ok := initiatorAccountsByType[accountType] - if !ok { - return errors.New("previous temp account record missing") - } - - // Find the open and close actions - - var openAction *transactionpb.Action - for _, action := range actions { - switch typed := action.Type.(type) { - case *transactionpb.Action_OpenAccount: - if typed.OpenAccount.AccountType == accountType { - if openAction != nil { - return newIntentValidationErrorf("multiple open actions for %s account type", accountType) - } - - openAction = action - } - } - } - - if openAction == nil { - return newIntentValidationErrorf("open account action for %s account type missing", accountType) - } - - if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatorOwnerAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "owner must be %s", initiatorOwnerAccount.PublicKey().ToBase58()) - } - - if bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(openAction, "authority cannot be %s", initiatorOwnerAccount.PublicKey().ToBase58()) - } - - expectedIndex := prevTempAccountRecords[0].General.Index + 1 - if openAction.GetOpenAccount().Index != expectedIndex { - return newActionValidationErrorf(openAction, "next derivation expected to be %d", expectedIndex) - } - - expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) - if err != nil { - return err - } - - if !bytes.Equal(openAction.GetOpenAccount().Token.Value, expectedVaultAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) - } - - return nil -} - // Assumes only one gift card account is opened per intent func validateGiftCardAccountOpened( ctx context.Context, data code_data.Provider, initiatorOwnerAccount *common.Account, - initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, expectedGiftCardVault *common.Account, actions []*transactionpb.Action, ) error { @@ -1263,41 +1217,6 @@ func validateFeePayments( return nil } -func validateMinimalTempIncomingAccountUsage(ctx context.Context, data code_data.Provider, accountInfo *account.Record) error { - if accountInfo.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { - return errors.New("expected a temporary incoming account") - } - - actionRecords, err := data.GetAllActionsByAddress(ctx, accountInfo.TokenAccount) - if err != nil && err != action.ErrActionNotFound { - return err - } - - var paymentCount int - for _, actionRecord := range actionRecords { - // Revoked actions don't count - if actionRecord.State == action.StateRevoked { - continue - } - - // Temp incoming accounts are always paid via no privacy withdraws - if actionRecord.ActionType != action.NoPrivacyWithdraw { - continue - } - - paymentCount += 1 - } - - // Should be coordinated with MustRotate flag in GetTokenAccountInfos - // - // todo: configurable - if paymentCount >= 2 { - // Important Note: Do not leak anything. Just say it isn't the latest. - return newStaleStateError("destination is not the latest temporary incoming account") - } - return nil -} - func validateClaimedGiftCard(ctx context.Context, data code_data.Provider, giftCardVaultAccount *common.Account, claimedAmount uint64) error { // // Part 1: Is the account a gift card? @@ -1344,9 +1263,7 @@ func validateClaimedGiftCard(ctx context.Context, data code_data.Provider, giftC } isManagedByCode := common.IsManagedByCode(ctx, timelockRecord) - if err != nil { - return err - } else if !isManagedByCode { + if !isManagedByCode { if timelockRecord.IsClosed() { // Better error messaging, since we know we'll never reopen the account // and the balance is guaranteed to be claimed (not necessarily through diff --git a/pkg/code/server/transaction/local_simulation.go b/pkg/code/server/transaction/local_simulation.go index 7cf2220e..f1364779 100644 --- a/pkg/code/server/transaction/local_simulation.go +++ b/pkg/code/server/transaction/local_simulation.go @@ -24,28 +24,23 @@ type TokenAccountSimulation struct { Opened bool OpenAction *transactionpb.Action - // todo: We need to handle CloseDormantAccount actions better. They're closed later, - // but there's no indication here in simulation that we could close it at our - // discretion. - Closed bool - CloseAction *transactionpb.Action + Closed bool + CloseAction *transactionpb.Action + IsAutoReturned bool } // todo: Make it easier to extract accounts from a TransferSimulation (see some fee payment validation logic) type TransferSimulation struct { - Action *transactionpb.Action - IsPrivate bool - IsWithdraw bool - IsFee bool - DeltaQuarks int64 + Action *transactionpb.Action + IsPrivate bool + IsWithdraw bool + IsFee bool + IsAutoReturn bool + DeltaQuarks int64 } // LocalSimulation simulates actions as if they were executed on the blockchain // taking into account cached Code DB state. -// -// Note: This doesn't currently incoporate accounts being closed by us. This is -// fine because we only close temporary accounts during rotation. We already have -// good validation for this, so it's fine for now. func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*transactionpb.Action) (*LocalSimulationResult, error) { result := &LocalSimulationResult{ SimulationsByAccount: make(map[string]TokenAccountSimulation), @@ -118,9 +113,6 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr Transfers: []TransferSimulation{ { Action: action, - IsPrivate: false, - IsWithdraw: false, - IsFee: false, DeltaQuarks: -int64(amount), }, }, @@ -130,9 +122,6 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr Transfers: []TransferSimulation{ { Action: action, - IsPrivate: false, - IsWithdraw: false, - IsFee: false, DeltaQuarks: int64(amount), }, }, @@ -159,8 +148,6 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr Transfers: []TransferSimulation{ { Action: action, - IsPrivate: false, - IsWithdraw: false, IsFee: true, DeltaQuarks: -int64(amount), }, @@ -199,25 +186,24 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr TokenAccount: source, Transfers: []TransferSimulation{ { - Action: action, - IsPrivate: false, - IsWithdraw: true, - IsFee: false, - DeltaQuarks: -int64(amount), + Action: action, + IsWithdraw: true, + IsAutoReturn: typedAction.NoPrivacyWithdraw.IsAutoReturn, + DeltaQuarks: -int64(amount), }, }, - Closed: true, - CloseAction: action, + Closed: true, + CloseAction: action, + IsAutoReturned: typedAction.NoPrivacyWithdraw.IsAutoReturn, }, TokenAccountSimulation{ TokenAccount: destination, Transfers: []TransferSimulation{ { - Action: action, - IsPrivate: false, - IsWithdraw: true, - IsFee: false, - DeltaQuarks: int64(amount), + Action: action, + IsWithdraw: true, + IsAutoReturn: typedAction.NoPrivacyWithdraw.IsAutoReturn, + DeltaQuarks: int64(amount), }, }, }, @@ -239,7 +225,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr // some basic level of validation. for _, simulation := range simulations { for _, txn := range simulation.Transfers { - // Attempt to transfer 0 Kin + // Attempt to transfer 0 quarks if txn.DeltaQuarks == 0 { return nil, newActionValidationError(action, "transaction with 0 quarks") } @@ -267,9 +253,9 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr return nil, newActionValidationError(action, "account is already closed in another action") } - // Attempt to send/receive Kin to a closed account + // Attempt to send/receive funds to a closed account if combined.Closed && len(simulation.Transfers) > 0 { - return nil, newActionValidationError(action, "account is closed and cannot send/receive kin") + return nil, newActionValidationError(action, "account is closed and cannot send/receive funds") } combined.Transfers = append(combined.Transfers, simulation.Transfers...) @@ -281,6 +267,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr if simulation.Closed { combined.CloseAction = simulation.CloseAction } + combined.IsAutoReturned = combined.IsAutoReturned || simulation.IsAutoReturned } else { combined = simulation } @@ -389,9 +376,13 @@ func (s LocalSimulationResult) GetClosedAccounts() []TokenAccountSimulation { return simulations } -func (s TokenAccountSimulation) GetDeltaQuarks() int64 { +func (s TokenAccountSimulation) GetDeltaQuarks(includeAutoReturns bool) int64 { var res int64 for _, txn := range s.Transfers { + if !includeAutoReturns && txn.IsAutoReturn { + continue + } + res += txn.DeltaQuarks } return res @@ -447,6 +438,16 @@ func (s TokenAccountSimulation) GetWithdraws() []TransferSimulation { return transfers } +func (s TokenAccountSimulation) GetAutoReturns() []TransferSimulation { + var transfers []TransferSimulation + for _, transfer := range s.Transfers { + if transfer.IsAutoReturn { + transfers = append(transfers, transfer) + } + } + return transfers +} + func (s LocalSimulationResult) GetFeePayments() []TransferSimulation { var transfers []TransferSimulation for _, tokenAccountSimulation := range s.SimulationsByAccount { @@ -576,3 +577,25 @@ func (s LocalSimulationResult) CountFeePayments() int { } return count } + +func FilterAutoReturnedAccounts(in []TokenAccountSimulation) []TokenAccountSimulation { + var out []TokenAccountSimulation + for _, account := range in { + if account.IsAutoReturned { + continue + } + out = append(out, account) + } + return out +} + +func FilterAutoReturnTransfers(in []TransferSimulation) []TransferSimulation { + var out []TransferSimulation + for _, transfer := range in { + if transfer.IsAutoReturn { + continue + } + out = append(out, transfer) + } + return out +} diff --git a/pkg/pointer/pointer.go b/pkg/pointer/pointer.go index a3f8da02..bcb5b5cd 100644 --- a/pkg/pointer/pointer.go +++ b/pkg/pointer/pointer.go @@ -32,6 +32,36 @@ func StringCopy(value *string) *string { return String(*value) } +// Uint32 returns a pointer to the provided uint32 value +func Uint32(value uint32) *uint32 { + return &value +} + +// Uint32OrDefault returns the pointer if not nil, otherwise the default value +func Uint32OrDefault(value *uint32, defaultValue uint32) *uint32 { + if value != nil { + return value + } + return &defaultValue +} + +// Uint32IfValid returns a pointer to the value if it's valid, otherwise nil +func Uint32IfValid(valid bool, value uint32) *uint32 { + if valid { + return &value + } + return nil +} + +// Uint32Copy returns a pointer that's a copy of the provided value +func Uint32Copy(value *uint32) *uint32 { + if value == nil { + return nil + } + + return Uint32(*value) +} + // Uint64 returns a pointer to the provided uint64 value func Uint64(value uint64) *uint64 { return &value