Skip to content

Commit 9688d32

Browse files
committed
Implement VoidGiftCard RPC
1 parent 928dcc1 commit 9688d32

File tree

5 files changed

+172
-74
lines changed

5 files changed

+172
-74
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.20250423160200-d52845b4298f
8+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250429171947-b8896029c856
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.20250423160200-d52845b4298f h1:bSvfqD5gy9n/xrYtALu1yV5oBEDf06mi/mkDkvBS29A=
82-
github.com/code-payments/code-protobuf-api v1.19.1-0.20250423160200-d52845b4298f/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs=
81+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250429171947-b8896029c856 h1:qHd5MHS7948r1DJgY9wRRuNIrISLmcG7SK3I8ui7ilc=
82+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250429171947-b8896029c856/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: 71 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package async_account
33
import (
44
"context"
55
"crypto/sha256"
6+
"database/sql"
67
"errors"
78
"math"
89
"sync"
@@ -97,7 +98,7 @@ func (p *service) maybeInitiateGiftCardAutoReturn(ctx context.Context, accountIn
9798
log.Trace("gift card is claimed and will be removed from worker queue")
9899

99100
// Cleanup anything related to gift card auto-return, since it cannot be scheduled
100-
err = p.initiateProcessToCleanupGiftCardAutoReturn(ctx, giftCardVaultAccount)
101+
err = initiateProcessToCleanupGiftCardAutoReturn(ctx, p.data, giftCardVaultAccount)
101102
if err != nil {
102103
log.WithError(err).Warn("failure cleaning up auto-return action")
103104
return err
@@ -123,7 +124,7 @@ func (p *service) maybeInitiateGiftCardAutoReturn(ctx context.Context, accountIn
123124
// There's no action to claim the gift card and the expiry window has been met.
124125
// It's time to initiate the process of auto-returning the funds back to the
125126
// issuer.
126-
err = p.initiateProcessToAutoReturnGiftCard(ctx, giftCardVaultAccount)
127+
err = InitiateProcessToAutoReturnGiftCard(ctx, p.data, giftCardVaultAccount, false)
127128
if err != nil {
128129
log.WithError(err).Warn("failure initiating process to return gift card balance to issuer")
129130
return err
@@ -133,84 +134,91 @@ func (p *service) maybeInitiateGiftCardAutoReturn(ctx context.Context, accountIn
133134

134135
// Note: This is the first instance of handling a conditional action, and could be
135136
// a good guide for similar actions in the future.
136-
func (p *service) initiateProcessToAutoReturnGiftCard(ctx context.Context, giftCardVaultAccount *common.Account) error {
137-
giftCardIssuedIntent, err := p.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVaultAccount.PublicKey().ToBase58())
138-
if err != nil {
139-
return err
140-
}
137+
//
138+
// todo: This probably belongs somewhere more common
139+
func InitiateProcessToAutoReturnGiftCard(ctx context.Context, data code_data.Provider, giftCardVaultAccount *common.Account, isVoidedByUser bool) error {
140+
return data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error {
141+
giftCardIssuedIntent, err := data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVaultAccount.PublicKey().ToBase58())
142+
if err != nil {
143+
return err
144+
}
141145

142-
autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58())
143-
if err != nil {
144-
return err
145-
}
146+
autoReturnAction, err := data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58())
147+
if err != nil {
148+
return err
149+
}
150+
if autoReturnAction.State != action.StateUnknown {
151+
return nil
152+
}
146153

147-
autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId)
148-
if err != nil {
149-
return err
150-
}
154+
autoReturnFulfillment, err := data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId)
155+
if err != nil {
156+
return err
157+
}
151158

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-
}
159+
// Add a intent record to show the funds being returned back to the issuer
160+
err = insertAutoReturnIntentRecord(ctx, data, giftCardIssuedIntent, isVoidedByUser)
161+
if err != nil {
162+
return err
163+
}
157164

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-
}
165+
// We need to update pre-sorting because auto-return fulfillments are always
166+
// inserted at the very last spot in the line.
167+
//
168+
// Must be the first thing to succeed! By pre-sorting this to the end of
169+
// the gift card issued intent, we ensure the auto-return is blocked on any
170+
// fulfillments to setup the gift card. We'll also guarantee that subsequent
171+
// intents that utilize the primary account as a source of funds will be blocked
172+
// by the auto-return.
173+
err = updateAutoReturnFulfillmentPreSorting(
174+
ctx,
175+
data,
176+
autoReturnFulfillment[0],
177+
giftCardIssuedIntent.Id,
178+
math.MaxInt32,
179+
0,
180+
)
181+
if err != nil {
182+
return err
183+
}
177184

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-
}
185+
// This will update the action's quantity, so balance changes are reflected. We
186+
// also unblock fulfillment scheduling by moving the action out of the unknown
187+
// state and into the pending state.
188+
err = scheduleAutoReturnAction(
189+
ctx,
190+
data,
191+
autoReturnAction,
192+
giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity,
193+
)
194+
if err != nil {
195+
return err
196+
}
190197

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])
198+
// This will trigger the fulfillment worker to poll for the fulfillment. This
199+
// should be the very last DB update called.
200+
return markFulfillmentAsActivelyScheduled(ctx, data, autoReturnFulfillment[0])
201+
})
194202
}
195203

196-
func (p *service) initiateProcessToCleanupGiftCardAutoReturn(ctx context.Context, giftCardVaultAccount *common.Account) error {
197-
autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58())
204+
func initiateProcessToCleanupGiftCardAutoReturn(ctx context.Context, data code_data.Provider, giftCardVaultAccount *common.Account) error {
205+
autoReturnAction, err := data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58())
198206
if err != nil {
199207
return err
200208
}
201209

202-
autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId)
210+
autoReturnFulfillment, err := data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId)
203211
if err != nil {
204212
return err
205213
}
206214

207-
err = markActionAsRevoked(ctx, p.data, autoReturnAction)
215+
err = markActionAsRevoked(ctx, data, autoReturnAction)
208216
if err != nil {
209217
return err
210218
}
211219

212220
// The sequencer will handle state transition and any cleanup
213-
return markFulfillmentAsActivelyScheduled(ctx, p.data, autoReturnFulfillment[0])
221+
return markFulfillmentAsActivelyScheduled(ctx, data, autoReturnFulfillment[0])
214222
}
215223

216224
func markAutoReturnCheckComplete(ctx context.Context, data code_data.Provider, record *account.Record) error {
@@ -274,7 +282,7 @@ func updateAutoReturnFulfillmentPreSorting(
274282
return data.UpdateFulfillment(ctx, fulfillmentRecord)
275283
}
276284

277-
func insertAutoReturnIntentRecord(ctx context.Context, data code_data.Provider, giftCardIssuedIntent *intent.Record) error {
285+
func insertAutoReturnIntentRecord(ctx context.Context, data code_data.Provider, giftCardIssuedIntent *intent.Record, isVoidedByUser bool) error {
278286
usdExchangeRecord, err := data.GetExchangeRate(ctx, currency.USD, time.Now())
279287
if err != nil {
280288
return err
@@ -293,8 +301,9 @@ func insertAutoReturnIntentRecord(ctx context.Context, data code_data.Provider,
293301
Source: giftCardIssuedIntent.SendPublicPaymentMetadata.DestinationTokenAccount,
294302
Quantity: giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity,
295303

296-
IsRemoteSend: true,
297-
IsReturned: true,
304+
IsRemoteSend: true,
305+
IsIssuerVoidingGiftCard: isVoidedByUser,
306+
IsReturned: !isVoidedByUser,
298307

299308
OriginalExchangeCurrency: giftCardIssuedIntent.SendPublicPaymentMetadata.ExchangeCurrency,
300309
OriginalExchangeRate: giftCardIssuedIntent.SendPublicPaymentMetadata.ExchangeRate,

pkg/code/server/transaction/intent.go

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1"
2020
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
2121

22+
async_account "github.com/code-payments/code-server/pkg/code/async/account"
2223
"github.com/code-payments/code-server/pkg/code/common"
2324
"github.com/code-payments/code-server/pkg/code/data/account"
2425
"github.com/code-payments/code-server/pkg/code/data/action"
@@ -952,10 +953,9 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact
952953
metadata = &transactionpb.Metadata{
953954
Type: &transactionpb.Metadata_ReceivePaymentsPublicly{
954955
ReceivePaymentsPublicly: &transactionpb.ReceivePaymentsPubliclyMetadata{
955-
Source: sourceAccount.ToProto(),
956-
Quarks: intentRecord.ReceivePaymentsPubliclyMetadata.Quantity,
957-
IsRemoteSend: intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend,
958-
IsIssuerVoidingGiftCard: intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard,
956+
Source: sourceAccount.ToProto(),
957+
Quarks: intentRecord.ReceivePaymentsPubliclyMetadata.Quantity,
958+
IsRemoteSend: intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend,
959959
ExchangeData: &transactionpb.ExchangeData{
960960
Currency: string(intentRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeCurrency),
961961
ExchangeRate: intentRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate,
@@ -1077,3 +1077,96 @@ func (s *transactionServer) CanWithdrawToAccount(ctx context.Context, req *trans
10771077
RequiresInitialization: requiresInitialization,
10781078
}, nil
10791079
}
1080+
1081+
func (s *transactionServer) VoidGiftCard(ctx context.Context, req *transactionpb.VoidGiftCardRequest) (*transactionpb.VoidGiftCardResponse, error) {
1082+
log := s.log.WithField("method", "VoidGiftCard")
1083+
log = client.InjectLoggingMetadata(ctx, log)
1084+
1085+
owner, err := common.NewAccountFromProto(req.Owner)
1086+
if err != nil {
1087+
log.WithError(err).Warn("invalid owner account")
1088+
return nil, status.Error(codes.Internal, "")
1089+
}
1090+
log = log.WithField("owner_account", owner.PublicKey().ToBase58())
1091+
1092+
giftCardVault, err := common.NewAccountFromProto(req.GiftCardVault)
1093+
if err != nil {
1094+
log.WithError(err).Warn("invalid owner account")
1095+
return nil, status.Error(codes.Internal, "")
1096+
}
1097+
log = log.WithField("gift_card_vault_account", giftCardVault.PublicKey().ToBase58())
1098+
1099+
signature := req.Signature
1100+
req.Signature = nil
1101+
if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil {
1102+
return nil, err
1103+
}
1104+
1105+
accountInfoRecord, err := s.data.GetAccountInfoByTokenAddress(ctx, giftCardVault.PublicKey().ToBase58())
1106+
switch err {
1107+
case nil:
1108+
if accountInfoRecord.AccountType != commonpb.AccountType_REMOTE_SEND_GIFT_CARD {
1109+
return &transactionpb.VoidGiftCardResponse{
1110+
Result: transactionpb.VoidGiftCardResponse_NOT_FOUND,
1111+
}, nil
1112+
}
1113+
case account.ErrAccountInfoNotFound:
1114+
return &transactionpb.VoidGiftCardResponse{
1115+
Result: transactionpb.VoidGiftCardResponse_NOT_FOUND,
1116+
}, nil
1117+
default:
1118+
log.WithError(err).Warn("failure getting gift card account info")
1119+
return nil, status.Error(codes.Internal, "")
1120+
}
1121+
1122+
giftCardIssuedIntentRecord, err := s.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVault.PublicKey().ToBase58())
1123+
if err != nil {
1124+
log.WithError(err).Warn("failure getting gift card issued intent record")
1125+
return nil, status.Error(codes.Internal, "")
1126+
} else if giftCardIssuedIntentRecord.InitiatorOwnerAccount != owner.PublicKey().ToBase58() {
1127+
return &transactionpb.VoidGiftCardResponse{
1128+
Result: transactionpb.VoidGiftCardResponse_DENIED,
1129+
}, nil
1130+
}
1131+
1132+
if time.Since(accountInfoRecord.CreatedAt) > async_account.GiftCardExpiry-15*time.Minute {
1133+
return &transactionpb.VoidGiftCardResponse{
1134+
Result: transactionpb.VoidGiftCardResponse_OK,
1135+
}, nil
1136+
}
1137+
1138+
giftCardLock := s.giftCardLocks.Get(giftCardVault.PublicKey().ToBytes())
1139+
giftCardLock.Lock()
1140+
defer giftCardLock.Unlock()
1141+
1142+
claimedActionRecord, err := s.data.GetGiftCardClaimedAction(ctx, giftCardVault.PublicKey().ToBase58())
1143+
if err == nil {
1144+
ownerTimelockAccounts, err := owner.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount)
1145+
if err != nil {
1146+
log.WithError(err).Warn("failure getting owner timelock accounts")
1147+
return nil, status.Error(codes.Internal, "")
1148+
}
1149+
1150+
if *claimedActionRecord.Destination != ownerTimelockAccounts.Vault.PublicKey().ToBase58() {
1151+
return &transactionpb.VoidGiftCardResponse{
1152+
Result: transactionpb.VoidGiftCardResponse_CLAIMED_BY_OTHER_USER,
1153+
}, nil
1154+
}
1155+
return &transactionpb.VoidGiftCardResponse{
1156+
Result: transactionpb.VoidGiftCardResponse_OK,
1157+
}, nil
1158+
} else if err != action.ErrActionNotFound {
1159+
log.WithError(err).Warn("failure getting gift card claimed action")
1160+
return nil, status.Error(codes.Internal, "")
1161+
}
1162+
1163+
err = async_account.InitiateProcessToAutoReturnGiftCard(ctx, s.data, giftCardVault, true)
1164+
if err != nil {
1165+
log.WithError(err).Warn("failure scheduling auto-return action")
1166+
return nil, status.Error(codes.Internal, "")
1167+
}
1168+
1169+
return &transactionpb.VoidGiftCardResponse{
1170+
Result: transactionpb.VoidGiftCardResponse_OK,
1171+
}, nil
1172+
}

pkg/code/server/transaction/intent_handler.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Cont
668668

669669
IsRemoteSend: typedProtoMetadata.IsRemoteSend,
670670
IsReturned: false,
671-
IsIssuerVoidingGiftCard: typedProtoMetadata.IsIssuerVoidingGiftCard,
671+
IsIssuerVoidingGiftCard: false,
672672

673673
OriginalExchangeCurrency: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.ExchangeCurrency,
674674
OriginalExchangeRate: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.ExchangeRate,
@@ -677,10 +677,6 @@ func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Cont
677677
UsdMarketValue: usdExchangeRecord.Rate * float64(typedProtoMetadata.Quarks) / float64(common.CoreMintQuarksPerUnit),
678678
}
679679

680-
if intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard && intentRecord.InitiatorOwnerAccount != giftCardIssuedIntentRecord.InitiatorOwnerAccount {
681-
return newIntentValidationError("only the issuer can void the gift card")
682-
}
683-
684680
return nil
685681
}
686682

0 commit comments

Comments
 (0)