Skip to content

Commit 31076ea

Browse files
authored
Implement remote send for the VM (#192)
1 parent f560e74 commit 31076ea

File tree

27 files changed

+670
-722
lines changed

27 files changed

+670
-722
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.23.0
55
require (
66
github.com/aws/aws-sdk-go-v2 v0.17.0
77
github.com/bits-and-blooms/bloom/v3 v3.1.0
8-
github.com/code-payments/code-protobuf-api v1.19.1-0.20250416185804-64d2132a62f2
8+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250423160200-d52845b4298f
99
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba
1010
github.com/emirpasic/gods v1.12.0
1111
github.com/envoyproxy/protoc-gen-validate v1.2.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
7878
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
7979
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
8080
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
81-
github.com/code-payments/code-protobuf-api v1.19.1-0.20250416185804-64d2132a62f2 h1:L2nVJRDyqbfx9DUUD7B0Es9JICjdwCjrJOdtmxNwAcs=
82-
github.com/code-payments/code-protobuf-api v1.19.1-0.20250416185804-64d2132a62f2/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs=
81+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250423160200-d52845b4298f h1:bSvfqD5gy9n/xrYtALu1yV5oBEDf06mi/mkDkvBS29A=
82+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250423160200-d52845b4298f/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs=
8383
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba h1:Bkp+gmeb6Y2PWXfkSCTMBGWkb2P1BujRDSjWeI+0j5I=
8484
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba/go.mod h1:jSiifpiBpyBQ8q2R0MGEbkSgWC6sbdRTyDBntmW+j1E=
8585
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw=

pkg/code/async/account/gift_card.go

Lines changed: 150 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package async_account
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"errors"
7+
"math"
68
"sync"
79
"time"
810

11+
"github.com/mr-tron/base58"
912
"github.com/newrelic/go-agent/v3/newrelic"
1013
"github.com/sirupsen/logrus"
1114

@@ -15,13 +18,14 @@ import (
1518
code_data "github.com/code-payments/code-server/pkg/code/data"
1619
"github.com/code-payments/code-server/pkg/code/data/account"
1720
"github.com/code-payments/code-server/pkg/code/data/action"
21+
"github.com/code-payments/code-server/pkg/code/data/fulfillment"
22+
"github.com/code-payments/code-server/pkg/code/data/intent"
23+
"github.com/code-payments/code-server/pkg/currency"
1824
"github.com/code-payments/code-server/pkg/metrics"
25+
"github.com/code-payments/code-server/pkg/pointer"
1926
"github.com/code-payments/code-server/pkg/retry"
2027
)
2128

22-
// todo: Is this even relevant anymore with the VM? If so, we need new logic because
23-
// closing dormant accounts is no longer a thing with the VM.
24-
2529
const (
2630
giftCardAutoReturnIntentPrefix = "auto-return-gc-"
2731
giftCardExpiry = 24 * time.Hour
@@ -39,7 +43,8 @@ func (p *service) giftCardAutoReturnWorker(serviceCtx context.Context, interval
3943
defer m.End()
4044
tracedCtx := newrelic.NewContext(serviceCtx, m)
4145

42-
records, err := p.data.GetPrioritizedAccountInfosRequiringAutoReturnCheck(tracedCtx, giftCardExpiry, 10)
46+
// todo: configurable batch size
47+
records, err := p.data.GetPrioritizedAccountInfosRequiringAutoReturnCheck(tracedCtx, giftCardExpiry, 32)
4348
if err == account.ErrAccountInfoNotFound {
4449
return nil
4550
} else if err != nil {
@@ -91,13 +96,14 @@ func (p *service) maybeInitiateGiftCardAutoReturn(ctx context.Context, accountIn
9196
if err == nil {
9297
log.Trace("gift card is claimed and will be removed from worker queue")
9398

94-
// Gift card is claimed, so take it out of the worker queue. The auto-return
95-
// action and fulfillment will be revoked in the fulfillment worker from generic
96-
// account closing flows.
97-
//
98-
// Note: It is possible the original issuer "claimed" the gift card. This is
99-
// actually ideal because funds move in a more private manner through the
100-
// temp incoming account versus the primary account.
99+
// Cleanup anything related to gift card auto-return, since it cannot be scheduled
100+
err = p.initiateProcessToCleanupGiftCardAutoReturn(ctx, giftCardVaultAccount)
101+
if err != nil {
102+
log.WithError(err).Warn("failure cleaning up auto-return action")
103+
return err
104+
}
105+
106+
// Gift card is claimed, so take it out of the worker queue.
101107
return markAutoReturnCheckComplete(ctx, p.data, accountInfoRecord)
102108
} else if err != action.ErrActionNotFound {
103109
return err
@@ -128,67 +134,82 @@ func (p *service) maybeInitiateGiftCardAutoReturn(ctx context.Context, accountIn
128134
// Note: This is the first instance of handling a conditional action, and could be
129135
// a good guide for similar actions in the future.
130136
func (p *service) initiateProcessToAutoReturnGiftCard(ctx context.Context, giftCardVaultAccount *common.Account) error {
131-
/*
132-
giftCardIssuedIntent, err := p.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVaultAccount.PublicKey().ToBase58())
133-
if err != nil {
134-
return err
135-
}
137+
giftCardIssuedIntent, err := p.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVaultAccount.PublicKey().ToBase58())
138+
if err != nil {
139+
return err
140+
}
136141

137-
autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58())
138-
if err != nil {
139-
return err
140-
}
142+
autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58())
143+
if err != nil {
144+
return err
145+
}
141146

142-
autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId)
143-
if err != nil {
144-
return err
145-
}
147+
autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId)
148+
if err != nil {
149+
return err
150+
}
146151

147-
// Add a payment history item to show the funds being returned back to the issuer
148-
err = insertAutoReturnPaymentHistoryItem(ctx, p.data, giftCardIssuedIntent)
149-
if err != nil {
150-
return err
151-
}
152+
// Add a intent record to show the funds being returned back to the issuer
153+
err = insertAutoReturnIntentRecord(ctx, p.data, giftCardIssuedIntent)
154+
if err != nil {
155+
return err
156+
}
152157

153-
// We need to update pre-sorting because close dormant fulfillments are always
154-
// inserted at the very last spot in the line.
155-
//
156-
// Must be the first thing to succeed! We cannot risk a deposit back into the
157-
// organizer to win a race in scheduling. By pre-sorting this to the end of
158-
// the gift card issued intent, we ensure the auto-return is blocked on any
159-
// fulfillments to setup the gift card. We'll also guarantee that subsequent
160-
// intents that utilize the primary account as a source of funds will be blocked
161-
// by the auto-return.
162-
err = updateCloseDormantAccountFulfillmentPreSorting(
163-
ctx,
164-
p.data,
165-
autoReturnFulfillment[0],
166-
giftCardIssuedIntent.Id,
167-
math.MaxInt32,
168-
0,
169-
)
170-
if err != nil {
171-
return err
172-
}
158+
// We need to update pre-sorting because auto-return fulfillments are always
159+
// inserted at the very last spot in the line.
160+
//
161+
// Must be the first thing to succeed! By pre-sorting this to the end of
162+
// the gift card issued intent, we ensure the auto-return is blocked on any
163+
// fulfillments to setup the gift card. We'll also guarantee that subsequent
164+
// intents that utilize the primary account as a source of funds will be blocked
165+
// by the auto-return.
166+
err = updateAutoReturnFulfillmentPreSorting(
167+
ctx,
168+
p.data,
169+
autoReturnFulfillment[0],
170+
giftCardIssuedIntent.Id,
171+
math.MaxInt32,
172+
0,
173+
)
174+
if err != nil {
175+
return err
176+
}
173177

174-
// This will update the action's quantity, so balance changes are reflected. We
175-
// also unblock fulfillment scheduling by moving the action out of the unknown
176-
// state and into the pending state.
177-
err = scheduleCloseDormantAccountAction(
178-
ctx,
179-
p.data,
180-
autoReturnAction,
181-
giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity,
182-
)
183-
if err != nil {
184-
return err
185-
}
178+
// This will update the action's quantity, so balance changes are reflected. We
179+
// also unblock fulfillment scheduling by moving the action out of the unknown
180+
// state and into the pending state.
181+
err = scheduleAutoReturnAction(
182+
ctx,
183+
p.data,
184+
autoReturnAction,
185+
giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity,
186+
)
187+
if err != nil {
188+
return err
189+
}
190+
191+
// This will trigger the fulfillment worker to poll for the fulfillment. This
192+
// should be the very last DB update called.
193+
return markFulfillmentAsActivelyScheduled(ctx, p.data, autoReturnFulfillment[0])
194+
}
186195

187-
// This will trigger the fulfillment worker to poll for the fulfillment. This
188-
// should be the very last DB update called.
189-
return markFulfillmentAsActivelyScheduled(ctx, p.data, autoReturnFulfillment[0])
190-
*/
191-
return errors.New("requires rewrite")
196+
func (p *service) initiateProcessToCleanupGiftCardAutoReturn(ctx context.Context, giftCardVaultAccount *common.Account) error {
197+
autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58())
198+
if err != nil {
199+
return err
200+
}
201+
202+
autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId)
203+
if err != nil {
204+
return err
205+
}
206+
207+
err = markActionAsRevoked(ctx, p.data, autoReturnAction)
208+
if err != nil {
209+
return err
210+
}
211+
212+
return markFulfillmentAsRevoked(ctx, p.data, autoReturnFulfillment[0])
192213
}
193214

194215
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
200221
return data.UpdateAccountInfo(ctx, record)
201222
}
202223

203-
/*
204-
205224
// Note: Structured like a generic utility because it could very well evolve
206225
// into that, but there's no reason to call this on anything else as of
207226
// writing this comment.
208-
func scheduleCloseDormantAccountAction(ctx context.Context, data code_data.Provider, actionRecord *action.Record, balance uint64) error {
209-
if actionRecord.ActionType != action.CloseDormantAccount {
210-
return errors.New("expected a close dormant account action")
227+
func scheduleAutoReturnAction(ctx context.Context, data code_data.Provider, actionRecord *action.Record, balance uint64) error {
228+
if actionRecord.ActionType != action.NoPrivacyWithdraw {
229+
return errors.New("expected a no privacy withdraw action")
211230
}
212231

213232
if actionRecord.State == action.StatePending {
@@ -226,16 +245,16 @@ func scheduleCloseDormantAccountAction(ctx context.Context, data code_data.Provi
226245
// Note: Structured like a generic utility because it could very well evolve
227246
// into that, but there's no reason to call this on anything else as of
228247
// writing this comment.
229-
func updateCloseDormantAccountFulfillmentPreSorting(
248+
func updateAutoReturnFulfillmentPreSorting(
230249
ctx context.Context,
231250
data code_data.Provider,
232251
fulfillmentRecord *fulfillment.Record,
233252
intentOrderingIndex uint64,
234253
actionOrderingIndex uint32,
235254
fulfillmentOrderingIndex uint32,
236255
) error {
237-
if fulfillmentRecord.FulfillmentType != fulfillment.CloseDormantTimelockAccount {
238-
return errors.New("expected a close dormant timelock account fulfillment")
256+
if fulfillmentRecord.FulfillmentType != fulfillment.NoPrivacyWithdraw {
257+
return errors.New("expected a no privacy withdraw fulfillment")
239258
}
240259

241260
if fulfillmentRecord.IntentOrderingIndex == intentOrderingIndex &&
@@ -254,24 +273,7 @@ func updateCloseDormantAccountFulfillmentPreSorting(
254273
return data.UpdateFulfillment(ctx, fulfillmentRecord)
255274
}
256275

257-
func markFulfillmentAsActivelyScheduled(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) error {
258-
if fulfillmentRecord.Id == 0 {
259-
return errors.New("fulfillment id is zero")
260-
}
261-
262-
if !fulfillmentRecord.DisableActiveScheduling {
263-
return nil
264-
}
265-
266-
if fulfillmentRecord.State != fulfillment.StateUnknown {
267-
return errors.New("expected fulfillment in unknown state")
268-
}
269-
270-
// Note: different than Save, since we don't have distributed locks
271-
return data.MarkFulfillmentAsActivelyScheduled(ctx, fulfillmentRecord.Id)
272-
}
273-
274-
func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Provider, giftCardIssuedIntent *intent.Record) error {
276+
func insertAutoReturnIntentRecord(ctx context.Context, data code_data.Provider, giftCardIssuedIntent *intent.Record) error {
275277
usdExchangeRecord, err := data.GetExchangeRate(ctx, currency.USD, time.Now())
276278
if err != nil {
277279
return err
@@ -280,29 +282,24 @@ func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Prov
280282
// We need to insert a faked completed public receive intent so it can appear
281283
// as a return in the user's payment history. Think of it as a server-initiated
282284
// intent on behalf of the user based on pre-approved conditional actions.
283-
//
284-
// Deprecated in favour of chats (for history purposes)
285-
//
286-
// todo: Should we remap the CloseDormantAccount action and fulfillments, then
287-
// tie the fulfillment/action state to the intent state? Just doing the
288-
// easiest thing for now to get auto-return out the door.
289285
intentRecord := &intent.Record{
290286
IntentId: getAutoReturnIntentId(giftCardIssuedIntent.IntentId),
291287
IntentType: intent.ReceivePaymentsPublicly,
292288

293289
InitiatorOwnerAccount: giftCardIssuedIntent.InitiatorOwnerAccount,
294290

295291
ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{
296-
Source: giftCardIssuedIntent.SendPrivatePaymentMetadata.DestinationTokenAccount,
297-
Quantity: giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity,
292+
Source: giftCardIssuedIntent.SendPublicPaymentMetadata.DestinationTokenAccount,
293+
Quantity: giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity,
294+
298295
IsRemoteSend: true,
299296
IsReturned: true,
300297

301-
OriginalExchangeCurrency: giftCardIssuedIntent.SendPrivatePaymentMetadata.ExchangeCurrency,
302-
OriginalExchangeRate: giftCardIssuedIntent.SendPrivatePaymentMetadata.ExchangeRate,
303-
OriginalNativeAmount: giftCardIssuedIntent.SendPrivatePaymentMetadata.NativeAmount,
298+
OriginalExchangeCurrency: giftCardIssuedIntent.SendPublicPaymentMetadata.ExchangeCurrency,
299+
OriginalExchangeRate: giftCardIssuedIntent.SendPublicPaymentMetadata.ExchangeRate,
300+
OriginalNativeAmount: giftCardIssuedIntent.SendPublicPaymentMetadata.NativeAmount,
304301

305-
UsdMarketValue: usdExchangeRecord.Rate * float64(common.FromCoreMintQuarks(giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity)),
302+
UsdMarketValue: usdExchangeRecord.Rate * float64(giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity) / float64(common.CoreMintQuarksPerUnit),
306303
},
307304

308305
State: intent.StateConfirmed,
@@ -312,10 +309,58 @@ func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Prov
312309
return data.SaveIntent(ctx, intentRecord)
313310
}
314311

312+
func markActionAsRevoked(ctx context.Context, data code_data.Provider, actionRecord *action.Record) error {
313+
if actionRecord.State == action.StateRevoked {
314+
return nil
315+
}
316+
317+
if actionRecord.State != action.StateUnknown {
318+
return errors.New("expected fulfillment in unknown state")
319+
}
320+
321+
actionRecord.State = action.StateRevoked
322+
323+
return data.UpdateAction(ctx, actionRecord)
324+
}
325+
326+
func markFulfillmentAsActivelyScheduled(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) error {
327+
if fulfillmentRecord.Id == 0 {
328+
return errors.New("fulfillment id is zero")
329+
}
330+
331+
if !fulfillmentRecord.DisableActiveScheduling {
332+
return nil
333+
}
334+
335+
if fulfillmentRecord.State != fulfillment.StateUnknown {
336+
return errors.New("expected fulfillment in unknown state")
337+
}
338+
339+
// Note: different than Save, since we don't have distributed locks
340+
return data.MarkFulfillmentAsActivelyScheduled(ctx, fulfillmentRecord.Id)
341+
}
342+
343+
func markFulfillmentAsRevoked(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) error {
344+
if fulfillmentRecord.Id == 0 {
345+
return errors.New("fulfillment id is zero")
346+
}
347+
348+
if fulfillmentRecord.State == fulfillment.StateRevoked {
349+
return nil
350+
}
351+
352+
if fulfillmentRecord.State != fulfillment.StateUnknown {
353+
return errors.New("expected fulfillment in unknown state")
354+
}
355+
356+
fulfillmentRecord.State = fulfillment.StateRevoked
357+
358+
return data.UpdateFulfillment(ctx, fulfillmentRecord)
359+
}
360+
315361
// Must be unique, but consistent for idempotency, and ideally fit in a 32
316362
// byte buffer.
317363
func getAutoReturnIntentId(originalIntentId string) string {
318364
hashed := sha256.Sum256([]byte(giftCardAutoReturnIntentPrefix + originalIntentId))
319365
return base58.Encode(hashed[:])
320366
}
321-
*/

0 commit comments

Comments
 (0)