Skip to content

Commit 1df099f

Browse files
committed
Support additional fee takers in messaging service
1 parent 5752c59 commit 1df099f

File tree

2 files changed

+102
-34
lines changed

2 files changed

+102
-34
lines changed

pkg/code/server/grpc/messaging/message_handler.go

Lines changed: 96 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,12 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende
161161
return newMessageValidationError("exchange data is nil")
162162
}
163163

164-
if len(typedMessage.AdditionalFees) > 0 {
165-
return newMessageValidationError("additional fee takers are not supported yet")
164+
var additionalFees []*paymentrequest.Fee
165+
for _, additionalFee := range typedMessage.AdditionalFees {
166+
additionalFees = append(additionalFees, &paymentrequest.Fee{
167+
DestinationTokenAccount: base58.Encode(additionalFee.Destination.Value),
168+
BasisPoints: uint16(additionalFee.FeeBps),
169+
})
166170
}
167171

168172
//
@@ -238,37 +242,27 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende
238242
return newMessageValidationError("original request isn't verified")
239243
}
240244

245+
if len(existingRequestRecord.Fees) != len(additionalFees) {
246+
return newMessageValidationErrorf("original request configured %d fee takers", len(existingRequestRecord.Fees))
247+
}
248+
for i, existingFee := range existingRequestRecord.Fees {
249+
if existingFee.DestinationTokenAccount != additionalFees[i].DestinationTokenAccount {
250+
return newMessageValidationErrorf("destination for fee at index %d mismatches original request", i)
251+
}
252+
253+
if existingFee.BasisPoints != additionalFees[i].BasisPoints {
254+
return newMessageValidationErrorf("basis points for fee at index %d mismatches original request", i)
255+
}
256+
}
257+
241258
h.recordAlreadyExists = true
242259
case paymentrequest.ErrPaymentRequestNotFound:
243260
//
244-
// Part 2.1: Requestor account must be a primary account (for trial use cases)
245-
// or an external account (for real production use cases)
261+
// Part 2.1: Requestor account must be a deposit or an external account
246262
//
247263

248-
accountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, requestorAccount.PublicKey().ToBase58())
249-
switch err {
250-
case nil:
251-
switch accountInfoRecord.AccountType {
252-
case commonpb.AccountType_PRIMARY:
253-
case commonpb.AccountType_RELATIONSHIP:
254-
if typedMessage.Verifier == nil {
255-
return newMessageValidationError("domain verification is required when requestor account is a relationship account")
256-
}
257-
258-
if *accountInfoRecord.RelationshipTo != asciiBaseDomain {
259-
return newMessageValidationErrorf("requestor account must have a relationship with %s", asciiBaseDomain)
260-
}
261-
default:
262-
return newMessageValidationError("requestor account must be a code deposit account")
263-
}
264-
case account.ErrAccountInfoNotFound:
265-
if !h.conf.disableBlockchainChecks.Get(ctx) {
266-
err := validateExternalKinTokenAccountWithinMessage(ctx, h.data, requestorAccount)
267-
if err != nil {
268-
return err
269-
}
270-
}
271-
default:
264+
err = h.validateDestinationAccount(ctx, requestorAccount, typedMessage.Verifier != nil, asciiBaseDomain)
265+
if err != nil {
272266
return err
273267
}
274268

@@ -291,6 +285,42 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende
291285
return err
292286
}
293287
}
288+
289+
//
290+
// Part 2.3: Fee structure validation
291+
//
292+
293+
// todo: need to validate the destination type is correct
294+
295+
var totalFeeBps uint32
296+
seenFeeTakers := make(map[string]interface{})
297+
for i, additionalFee := range additionalFees {
298+
feeTaker, err := common.NewAccountFromPublicKeyString(additionalFee.DestinationTokenAccount)
299+
if err != nil {
300+
return err
301+
}
302+
303+
totalFeeBps += uint32(additionalFee.BasisPoints)
304+
305+
if additionalFee.DestinationTokenAccount == requestorAccount.PublicKey().ToBase58() {
306+
return newMessageValidationErrorf("fee taker at index %d is the payment destination and should be omitted", i)
307+
}
308+
309+
_, ok := seenFeeTakers[additionalFee.DestinationTokenAccount]
310+
if ok {
311+
return newMessageValidationErrorf("fee taker at index %d appears multiple times and should be merged", i)
312+
}
313+
seenFeeTakers[additionalFee.DestinationTokenAccount] = struct{}{}
314+
315+
err = h.validateDestinationAccount(ctx, feeTaker, typedMessage.Verifier != nil, asciiBaseDomain)
316+
if err != nil {
317+
return err
318+
}
319+
}
320+
// todo: configurable
321+
if totalFeeBps > 5_000 {
322+
return newMessageValidationError("total fee percentage cannot exceed 50%")
323+
}
294324
default:
295325
return err
296326
}
@@ -370,6 +400,7 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende
370400
NativeAmount: pointer.Float64(nativeAmount),
371401
ExchangeRate: exchangeRate,
372402
Quantity: quarks,
403+
Fees: additionalFees,
373404

374405
CreatedAt: time.Now(),
375406
}
@@ -394,6 +425,43 @@ func (h *RequestToReceiveBillMessageHandler) OnSuccess(ctx context.Context) erro
394425
return h.data.CreateRequest(ctx, h.recordToSave)
395426
}
396427

428+
// todo: need to add context (ie. is it payment destination or fee taker) and improve error messaging
429+
func (h *RequestToReceiveBillMessageHandler) validateDestinationAccount(
430+
ctx context.Context,
431+
accountToValidate *common.Account,
432+
isVerified bool,
433+
asciiBaseDomain string,
434+
) error {
435+
accountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, accountToValidate.PublicKey().ToBase58())
436+
switch err {
437+
case nil:
438+
switch accountInfoRecord.AccountType {
439+
case commonpb.AccountType_PRIMARY:
440+
case commonpb.AccountType_RELATIONSHIP:
441+
if !isVerified {
442+
return newMessageValidationError("domain verification is required when using a relationship account")
443+
}
444+
445+
if *accountInfoRecord.RelationshipTo != asciiBaseDomain {
446+
return newMessageValidationErrorf("relationship account is not associated with %s", asciiBaseDomain)
447+
}
448+
default:
449+
return newMessageValidationError("code account must be a deposit account")
450+
}
451+
case account.ErrAccountInfoNotFound:
452+
if !h.conf.disableBlockchainChecks.Get(ctx) {
453+
err := validateExternalKinTokenAccountWithinMessage(ctx, h.data, accountToValidate)
454+
if err != nil {
455+
return err
456+
}
457+
}
458+
default:
459+
return err
460+
}
461+
462+
return nil
463+
}
464+
397465
type ClientRejectedPaymentMessageHandler struct {
398466
}
399467

pkg/code/server/grpc/messaging/server_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,14 @@ func TestSendMessage_RequestToReceiveBill_KinValue_Validation(t *testing.T) {
377377
env.client1.resetConf()
378378
env.client1.conf.simulateInvalidAccountType = true
379379
sendMessageCall := env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, false, false, true)
380-
sendMessageCall.assertInvalidMessageError(t, "requestor account must be a code deposit account")
380+
sendMessageCall.assertInvalidMessageError(t, "code account must be a deposit account")
381381
env.server1.assertNoMessages(t, rendezvousKey)
382382
env.server1.assertRequestRecordNotSaved(t, rendezvousKey)
383383

384384
env.client1.resetConf()
385385
env.client1.conf.simulateInvalidRelationship = true
386386
sendMessageCall = env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, false, true, false)
387-
sendMessageCall.assertInvalidMessageError(t, "requestor account must have a relationship with getcode.com")
387+
sendMessageCall.assertInvalidMessageError(t, "relationship account is not associated with getcode.com")
388388
env.server1.assertNoMessages(t, rendezvousKey)
389389
env.server1.assertRequestRecordNotSaved(t, rendezvousKey)
390390

@@ -461,7 +461,7 @@ func TestSendMessage_RequestToReceiveBill_KinValue_Validation(t *testing.T) {
461461

462462
env.client1.resetConf()
463463
sendMessageCall = env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, false, true, true)
464-
sendMessageCall.assertInvalidMessageError(t, "domain verification is required when requestor account is a relationship account")
464+
sendMessageCall.assertInvalidMessageError(t, "domain verification is required when using a relationship account")
465465
env.server1.assertNoMessages(t, rendezvousKey)
466466
env.server1.assertRequestRecordNotSaved(t, rendezvousKey)
467467

@@ -514,14 +514,14 @@ func TestSendMessage_RequestToReceiveBill_FiatValue_Validation(t *testing.T) {
514514
env.client1.resetConf()
515515
env.client1.conf.simulateInvalidAccountType = true
516516
sendMessageCall := env.client1.sendRequestToReceiveFiatBillMessage(t, rendezvousKey, false, false, true)
517-
sendMessageCall.assertInvalidMessageError(t, "requestor account must be a code deposit account")
517+
sendMessageCall.assertInvalidMessageError(t, "code account must be a deposit account")
518518
env.server1.assertNoMessages(t, rendezvousKey)
519519
env.server1.assertRequestRecordNotSaved(t, rendezvousKey)
520520

521521
env.client1.resetConf()
522522
env.client1.conf.simulateInvalidRelationship = true
523523
sendMessageCall = env.client1.sendRequestToReceiveFiatBillMessage(t, rendezvousKey, false, true, false)
524-
sendMessageCall.assertInvalidMessageError(t, "requestor account must have a relationship with getcode.com")
524+
sendMessageCall.assertInvalidMessageError(t, "relationship account is not associated with getcode.com")
525525
env.server1.assertNoMessages(t, rendezvousKey)
526526
env.server1.assertRequestRecordNotSaved(t, rendezvousKey)
527527

@@ -570,7 +570,7 @@ func TestSendMessage_RequestToReceiveBill_FiatValue_Validation(t *testing.T) {
570570

571571
env.client1.resetConf()
572572
sendMessageCall = env.client1.sendRequestToReceiveFiatBillMessage(t, rendezvousKey, false, true, true)
573-
sendMessageCall.assertInvalidMessageError(t, "domain verification is required when requestor account is a relationship account")
573+
sendMessageCall.assertInvalidMessageError(t, "domain verification is required when using a relationship account")
574574
env.server1.assertNoMessages(t, rendezvousKey)
575575
env.server1.assertRequestRecordNotSaved(t, rendezvousKey)
576576

0 commit comments

Comments
 (0)