@@ -161,8 +161,12 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende
161
161
return newMessageValidationError ("exchange data is nil" )
162
162
}
163
163
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
+ })
166
170
}
167
171
168
172
//
@@ -238,37 +242,27 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende
238
242
return newMessageValidationError ("original request isn't verified" )
239
243
}
240
244
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
+
241
258
h .recordAlreadyExists = true
242
259
case paymentrequest .ErrPaymentRequestNotFound :
243
260
//
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
246
262
//
247
263
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 {
272
266
return err
273
267
}
274
268
@@ -291,6 +285,42 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende
291
285
return err
292
286
}
293
287
}
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
+ }
294
324
default :
295
325
return err
296
326
}
@@ -370,6 +400,7 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende
370
400
NativeAmount : pointer .Float64 (nativeAmount ),
371
401
ExchangeRate : exchangeRate ,
372
402
Quantity : quarks ,
403
+ Fees : additionalFees ,
373
404
374
405
CreatedAt : time .Now (),
375
406
}
@@ -394,6 +425,43 @@ func (h *RequestToReceiveBillMessageHandler) OnSuccess(ctx context.Context) erro
394
425
return h .data .CreateRequest (ctx , h .recordToSave )
395
426
}
396
427
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
+
397
465
type ClientRejectedPaymentMessageHandler struct {
398
466
}
399
467
0 commit comments