diff --git a/lib/python/rvaspy/rvaspy/api_pb2.py b/lib/python/rvaspy/rvaspy/api_pb2.py index eec2931..27d40e1 100644 --- a/lib/python/rvaspy/rvaspy/api_pb2.py +++ b/lib/python/rvaspy/rvaspy/api_pb2.py @@ -20,7 +20,7 @@ syntax='proto3', serialized_options=b'Z2github.com/trisacrypto/testnet/pkg/rvasp/pb/v1;api', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\tapi.proto\x12\x08rvasp.v1\"&\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"B\n\x07\x41\x63\x63ount\x12\x16\n\x0ewallet_address\x18\x01 \x01(\t\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\"\xce\x01\n\x0bTransaction\x12%\n\noriginator\x18\x01 \x01(\x0b\x32\x11.rvasp.v1.Account\x12&\n\x0b\x62\x65neficiary\x18\x02 \x01(\x0b\x32\x11.rvasp.v1.Account\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x02\x12\x11\n\ttimestamp\x18\x04 \x01(\t\x12\x10\n\x08\x65nvelope\x18\x05 \x01(\t\x12\x10\n\x08identity\x18\x06 \x01(\t\x12)\n\x05state\x18\x07 \x01(\x0e\x32\x1a.rvasp.v1.TransactionState\"\xad\x01\n\x0fTransferRequest\x12\x0f\n\x07\x61\x63\x63ount\x18\x01 \x01(\t\x12\x13\n\x0b\x62\x65neficiary\x18\x02 \x01(\t\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x02\x12\x18\n\x10originating_vasp\x18\x04 \x01(\t\x12\x18\n\x10\x62\x65neficiary_vasp\x18\x05 \x01(\t\x12\x19\n\x11\x63heck_beneficiary\x18\x06 \x01(\x08\x12\x15\n\rexternal_demo\x18\x07 \x01(\x08\"[\n\rTransferReply\x12\x1e\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x0f.rvasp.v1.Error\x12*\n\x0btransaction\x18\x02 \x01(\x0b\x32\x15.rvasp.v1.Transaction\"Z\n\x0e\x41\x63\x63ountRequest\x12\x0f\n\x07\x61\x63\x63ount\x18\x01 \x01(\t\x12\x17\n\x0fno_transactions\x18\x02 \x01(\x08\x12\x0c\n\x04page\x18\x03 \x01(\r\x12\x10\n\x08per_page\x18\x04 \x01(\r\"\xc5\x01\n\x0c\x41\x63\x63ountReply\x12\x1e\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x0f.rvasp.v1.Error\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x16\n\x0ewallet_address\x18\x04 \x01(\t\x12\x0f\n\x07\x62\x61lance\x18\x05 \x01(\x02\x12\x11\n\tcompleted\x18\x06 \x01(\x04\x12\x0f\n\x07pending\x18\x07 \x01(\x04\x12+\n\x0ctransactions\x18\x08 \x03(\x0b\x32\x15.rvasp.v1.Transaction\"\xa9\x01\n\x07\x43ommand\x12\x1b\n\x04type\x18\x01 \x01(\x0e\x32\r.rvasp.v1.RPC\x12\n\n\x02id\x18\x02 \x01(\x04\x12\x0e\n\x06\x63lient\x18\x03 \x01(\t\x12-\n\x08transfer\x18\x0b \x01(\x0b\x32\x19.rvasp.v1.TransferRequestH\x00\x12+\n\x07\x61\x63\x63ount\x18\x0c \x01(\x0b\x32\x18.rvasp.v1.AccountRequestH\x00\x42\t\n\x07request\"\xe3\x01\n\x07Message\x12\x1b\n\x04type\x18\x01 \x01(\x0e\x32\r.rvasp.v1.RPC\x12\n\n\x02id\x18\x02 \x01(\x04\x12\x0e\n\x06update\x18\x03 \x01(\t\x12\x11\n\ttimestamp\x18\x04 \x01(\t\x12+\n\x08\x63\x61tegory\x18\x05 \x01(\x0e\x32\x19.rvasp.v1.MessageCategory\x12+\n\x08transfer\x18\x0b \x01(\x0b\x32\x17.rvasp.v1.TransferReplyH\x00\x12)\n\x07\x61\x63\x63ount\x18\x0c \x01(\x0b\x32\x16.rvasp.v1.AccountReplyH\x00\x42\x07\n\x05reply*\x8f\x01\n\x10TransactionState\x12\x0b\n\x07INVALID\x10\x00\x12\x0c\n\x08\x41WAITING\x10\x01\x12\x10\n\x0cPENDING_SENT\x10\x02\x12\x18\n\x14PENDING_ACKNOWLEDGED\x10\x03\x12\x0c\n\x08\x41\x43\x43\x45PTED\x10\x04\x12\n\n\x06\x46\x41ILED\x10\x05\x12\x0b\n\x07\x45XPIRED\x10\x06\x12\r\n\tCOMPLETED\x10\x07*+\n\x03RPC\x12\t\n\x05NORPC\x10\x00\x12\x0c\n\x08TRANSFER\x10\x01\x12\x0b\n\x07\x41\x43\x43OUNT\x10\x02*S\n\x0fMessageCategory\x12\n\n\x06LEDGER\x10\x00\x12\x0b\n\x07TRISADS\x10\x01\x12\x0c\n\x08TRISAP2P\x10\x02\x12\x0e\n\nBLOCKCHAIN\x10\x03\x12\t\n\x05\x45RROR\x10\x04\x32\x44\n\tTRISADemo\x12\x37\n\x0bLiveUpdates\x12\x11.rvasp.v1.Command\x1a\x11.rvasp.v1.Message(\x01\x30\x01\x32\x95\x01\n\x10TRISAIntegration\x12>\n\x08Transfer\x12\x19.rvasp.v1.TransferRequest\x1a\x17.rvasp.v1.TransferReply\x12\x41\n\rAccountStatus\x12\x18.rvasp.v1.AccountRequest\x1a\x16.rvasp.v1.AccountReplyB4Z2github.com/trisacrypto/testnet/pkg/rvasp/pb/v1;apib\x06proto3' + serialized_pb=b'\n\tapi.proto\x12\x08rvasp.v1\"&\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"B\n\x07\x41\x63\x63ount\x12\x16\n\x0ewallet_address\x18\x01 \x01(\t\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\"\xce\x01\n\x0bTransaction\x12%\n\noriginator\x18\x01 \x01(\x0b\x32\x11.rvasp.v1.Account\x12&\n\x0b\x62\x65neficiary\x18\x02 \x01(\x0b\x32\x11.rvasp.v1.Account\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x02\x12\x11\n\ttimestamp\x18\x04 \x01(\t\x12\x10\n\x08\x65nvelope\x18\x05 \x01(\t\x12\x10\n\x08identity\x18\x06 \x01(\t\x12)\n\x05state\x18\x07 \x01(\x0e\x32\x1a.rvasp.v1.TransactionState\"\xad\x01\n\x0fTransferRequest\x12\x0f\n\x07\x61\x63\x63ount\x18\x01 \x01(\t\x12\x13\n\x0b\x62\x65neficiary\x18\x02 \x01(\t\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x02\x12\x18\n\x10originating_vasp\x18\x04 \x01(\t\x12\x18\n\x10\x62\x65neficiary_vasp\x18\x05 \x01(\t\x12\x19\n\x11\x63heck_beneficiary\x18\x06 \x01(\x08\x12\x15\n\rexternal_demo\x18\x07 \x01(\x08\"[\n\rTransferReply\x12\x1e\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x0f.rvasp.v1.Error\x12*\n\x0btransaction\x18\x02 \x01(\x0b\x32\x15.rvasp.v1.Transaction\"Z\n\x0e\x41\x63\x63ountRequest\x12\x0f\n\x07\x61\x63\x63ount\x18\x01 \x01(\t\x12\x17\n\x0fno_transactions\x18\x02 \x01(\x08\x12\x0c\n\x04page\x18\x03 \x01(\r\x12\x10\n\x08per_page\x18\x04 \x01(\r\"\xc5\x01\n\x0c\x41\x63\x63ountReply\x12\x1e\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x0f.rvasp.v1.Error\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x16\n\x0ewallet_address\x18\x04 \x01(\t\x12\x0f\n\x07\x62\x61lance\x18\x05 \x01(\x02\x12\x11\n\tcompleted\x18\x06 \x01(\x04\x12\x0f\n\x07pending\x18\x07 \x01(\x04\x12+\n\x0ctransactions\x18\x08 \x03(\x0b\x32\x15.rvasp.v1.Transaction\"\xa9\x01\n\x07\x43ommand\x12\x1b\n\x04type\x18\x01 \x01(\x0e\x32\r.rvasp.v1.RPC\x12\n\n\x02id\x18\x02 \x01(\x04\x12\x0e\n\x06\x63lient\x18\x03 \x01(\t\x12-\n\x08transfer\x18\x0b \x01(\x0b\x32\x19.rvasp.v1.TransferRequestH\x00\x12+\n\x07\x61\x63\x63ount\x18\x0c \x01(\x0b\x32\x18.rvasp.v1.AccountRequestH\x00\x42\t\n\x07request\"\xe3\x01\n\x07Message\x12\x1b\n\x04type\x18\x01 \x01(\x0e\x32\r.rvasp.v1.RPC\x12\n\n\x02id\x18\x02 \x01(\x04\x12\x0e\n\x06update\x18\x03 \x01(\t\x12\x11\n\ttimestamp\x18\x04 \x01(\t\x12+\n\x08\x63\x61tegory\x18\x05 \x01(\x0e\x32\x19.rvasp.v1.MessageCategory\x12+\n\x08transfer\x18\x0b \x01(\x0b\x32\x17.rvasp.v1.TransferReplyH\x00\x12)\n\x07\x61\x63\x63ount\x18\x0c \x01(\x0b\x32\x16.rvasp.v1.AccountReplyH\x00\x42\x07\n\x05reply*\x9d\x01\n\x10TransactionState\x12\x0b\n\x07INVALID\x10\x00\x12\x0c\n\x08\x41WAITING\x10\x01\x12\x10\n\x0cPENDING_SENT\x10\x02\x12\x18\n\x14PENDING_ACKNOWLEDGED\x10\x03\x12\x0c\n\x08\x41\x43\x43\x45PTED\x10\x04\x12\n\n\x06\x46\x41ILED\x10\x05\x12\x0b\n\x07\x45XPIRED\x10\x06\x12\x0c\n\x08REJECTED\x10\x07\x12\r\n\tCOMPLETED\x10\x08*+\n\x03RPC\x12\t\n\x05NORPC\x10\x00\x12\x0c\n\x08TRANSFER\x10\x01\x12\x0b\n\x07\x41\x43\x43OUNT\x10\x02*S\n\x0fMessageCategory\x12\n\n\x06LEDGER\x10\x00\x12\x0b\n\x07TRISADS\x10\x01\x12\x0c\n\x08TRISAP2P\x10\x02\x12\x0e\n\nBLOCKCHAIN\x10\x03\x12\t\n\x05\x45RROR\x10\x04\x32\x44\n\tTRISADemo\x12\x37\n\x0bLiveUpdates\x12\x11.rvasp.v1.Command\x1a\x11.rvasp.v1.Message(\x01\x30\x01\x32\x95\x01\n\x10TRISAIntegration\x12>\n\x08Transfer\x12\x19.rvasp.v1.TransferRequest\x1a\x17.rvasp.v1.TransferReply\x12\x41\n\rAccountStatus\x12\x18.rvasp.v1.AccountRequest\x1a\x16.rvasp.v1.AccountReplyB4Z2github.com/trisacrypto/testnet/pkg/rvasp/pb/v1;apib\x06proto3' ) _TRANSACTIONSTATE = _descriptor.EnumDescriptor( @@ -66,7 +66,12 @@ type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( - name='COMPLETED', index=7, number=7, + name='REJECTED', index=7, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='COMPLETED', index=8, number=8, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), @@ -74,7 +79,7 @@ containing_type=None, serialized_options=None, serialized_start=1304, - serialized_end=1447, + serialized_end=1461, ) _sym_db.RegisterEnumDescriptor(_TRANSACTIONSTATE) @@ -104,8 +109,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1449, - serialized_end=1492, + serialized_start=1463, + serialized_end=1506, ) _sym_db.RegisterEnumDescriptor(_RPC) @@ -145,8 +150,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=1494, - serialized_end=1577, + serialized_start=1508, + serialized_end=1591, ) _sym_db.RegisterEnumDescriptor(_MESSAGECATEGORY) @@ -158,7 +163,8 @@ ACCEPTED = 4 FAILED = 5 EXPIRED = 6 -COMPLETED = 7 +REJECTED = 7 +COMPLETED = 8 NORPC = 0 TRANSFER = 1 ACCOUNT = 2 @@ -832,8 +838,8 @@ index=0, serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_start=1579, - serialized_end=1647, + serialized_start=1593, + serialized_end=1661, methods=[ _descriptor.MethodDescriptor( name='LiveUpdates', @@ -858,8 +864,8 @@ index=1, serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_start=1650, - serialized_end=1799, + serialized_start=1664, + serialized_end=1813, methods=[ _descriptor.MethodDescriptor( name='Transfer', diff --git a/pkg/rvasp/async.go b/pkg/rvasp/async.go index cf37b77..e3b0159 100644 --- a/pkg/rvasp/async.go +++ b/pkg/rvasp/async.go @@ -69,7 +69,9 @@ func (s *TRISA) handleAsync(ctx context.Context) { now := time.Now() txloop: - for _, tx := range transactions { + for _, transaction := range transactions { + tx := &transaction + // Verify pending transaction is old enough if now.Before(tx.NotBefore) { continue @@ -78,7 +80,7 @@ txloop: // Verify pending transaction has not expired if now.After(tx.NotAfter) { log.Info().Uint("id", tx.ID).Time("not_after", tx.NotAfter).Msg("transaction expired") - tx.State = pb.TransactionState_EXPIRED + tx.SetState(pb.TransactionState_EXPIRED) if err = s.parent.db.Save(&tx).Error; err != nil { log.Error().Err(err).Uint("id", tx.ID).Msg("could not save expired transaction") } @@ -86,13 +88,13 @@ txloop: } // Acknowledge the transaction with the originator - if err = s.acknowledgeTransaction(&tx); err != nil { + if err = s.acknowledgeTransaction(tx); err != nil { log.Warn().Err(err).Uint("id", tx.ID).Msg("could not acknowledge transaction") - tx.State = pb.TransactionState_FAILED + tx.SetState(pb.TransactionState_FAILED) } // Save the updated transaction in the database - if err = s.parent.db.Save(&tx).Error; err != nil { + if err = s.parent.db.Save(tx).Error; err != nil { log.Error().Err(err).Uint("id", tx.ID).Msg("could not save completed transaction") } } diff --git a/pkg/rvasp/db/db.go b/pkg/rvasp/db/db.go index 60fcb90..669a13e 100644 --- a/pkg/rvasp/db/db.go +++ b/pkg/rvasp/db/db.go @@ -111,13 +111,13 @@ func (d *DB) LookupWallet(address string) *gorm.DB { // MakeTransaction returns a new Transaction from the originator and beneficiary // wallet addresses. Note: this does not store the transaction in the database to allow // the caller to modify the transaction fields before storage. -func (d *DB) MakeTransaction(originator string, beneficiary string) (Transaction, error) { +func (d *DB) MakeTransaction(originator string, beneficiary string) (*Transaction, error) { var originatorIdentity, beneficiaryIdentity Identity // Fetch originator identity record if err := d.LookupIdentity(originator).FirstOrInit(&originatorIdentity, Identity{}).Error; err != nil { log.Error().Err(err).Msg("could not lookup originator identity") - return Transaction{}, status.Errorf(codes.FailedPrecondition, "could not lookup originator identity: %s", err) + return nil, status.Errorf(codes.FailedPrecondition, "could not lookup originator identity: %s", err) } // If originator identity does not exist then create it @@ -127,14 +127,14 @@ func (d *DB) MakeTransaction(originator string, beneficiary string) (Transaction if err := d.Create(&originatorIdentity).Error; err != nil { log.Error().Err(err).Msg("could not save originator identity") - return Transaction{}, status.Errorf(codes.FailedPrecondition, "could not save originator identity: %s", err) + return nil, status.Errorf(codes.FailedPrecondition, "could not save originator identity: %s", err) } } // Fetch beneficiary identity record if err := d.LookupIdentity(beneficiary).FirstOrInit(&beneficiaryIdentity, Identity{}).Error; err != nil { log.Error().Err(err).Msg("could not lookup beneficiary identity") - return Transaction{}, status.Errorf(codes.FailedPrecondition, "could not lookup beneficiary identity: %s", err) + return nil, status.Errorf(codes.FailedPrecondition, "could not lookup beneficiary identity: %s", err) } // If the beneficiary identity does not exist then create it @@ -144,15 +144,16 @@ func (d *DB) MakeTransaction(originator string, beneficiary string) (Transaction if err := d.Create(&beneficiaryIdentity).Error; err != nil { log.Error().Err(err).Msg("could not save beneficiary identity") - return Transaction{}, status.Errorf(codes.FailedPrecondition, "could not save beneficiary identity: %s", err) + return nil, status.Errorf(codes.FailedPrecondition, "could not save beneficiary identity: %s", err) } } - return Transaction{ + return &Transaction{ Envelope: uuid.New().String(), Originator: originatorIdentity, Beneficiary: beneficiaryIdentity, State: pb.TransactionState_AWAITING, + StateString: pb.TransactionState_AWAITING.String(), Timestamp: time.Now(), Vasp: d.vasp, }, nil @@ -260,6 +261,7 @@ type Transaction struct { Amount decimal.Decimal `gorm:"type:decimal(15,8)"` Debit bool `gorm:"not null"` State pb.TransactionState `gorm:"not null;default:0"` + StateString string `gorm:"column:state_string;not null"` Timestamp time.Time `gorm:"not null"` NotBefore time.Time `gorm:"not null"` NotAfter time.Time `gorm:"not null"` @@ -274,6 +276,12 @@ func (Transaction) TableName() string { return "transactions" } +// Set the transaction state to a new value +func (t *Transaction) SetState(state pb.TransactionState) { + t.State = state + t.StateString = state.String() +} + // Identity holds raw data for an originator or a beneficiary that was sent as // part of the transaction process. This should not be stored in the wallet since the // wallet is a representation of the local VASPs knowledge about customers and bercause diff --git a/pkg/rvasp/pb/v1/api.pb.go b/pkg/rvasp/pb/v1/api.pb.go index c6fb010..1b3fd4f 100644 --- a/pkg/rvasp/pb/v1/api.pb.go +++ b/pkg/rvasp/pb/v1/api.pb.go @@ -31,7 +31,8 @@ const ( TransactionState_ACCEPTED TransactionState = 4 // (Async) Originator has received the transaction acknowledgement from the beneficiary TransactionState_FAILED TransactionState = 5 // The transaction has failed TransactionState_EXPIRED TransactionState = 6 // The asynchronous transaction has expired before completion - TransactionState_COMPLETED TransactionState = 7 // The transaction is completed + TransactionState_REJECTED TransactionState = 7 // The transaction has been rejected + TransactionState_COMPLETED TransactionState = 8 // The transaction is completed ) // Enum value maps for TransactionState. @@ -44,7 +45,8 @@ var ( 4: "ACCEPTED", 5: "FAILED", 6: "EXPIRED", - 7: "COMPLETED", + 7: "REJECTED", + 8: "COMPLETED", } TransactionState_value = map[string]int32{ "INVALID": 0, @@ -54,7 +56,8 @@ var ( "ACCEPTED": 4, "FAILED": 5, "EXPIRED": 6, - "COMPLETED": 7, + "REJECTED": 7, + "COMPLETED": 8, } ) @@ -1092,7 +1095,7 @@ var file_rvasp_v1_api_proto_rawDesc = []byte{ 0x72, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x48, 0x00, 0x52, 0x07, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x2a, 0x8f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x2a, 0x9d, 0x01, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x57, 0x41, 0x49, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x10, @@ -1101,33 +1104,34 @@ var file_rvasp_v1_api_proto_rawDesc = []byte{ 0x4f, 0x57, 0x4c, 0x45, 0x44, 0x47, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, - 0x06, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x07, - 0x2a, 0x2b, 0x0a, 0x03, 0x52, 0x50, 0x43, 0x12, 0x09, 0x0a, 0x05, 0x4e, 0x4f, 0x52, 0x50, 0x43, - 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x10, 0x01, - 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x43, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x02, 0x2a, 0x53, 0x0a, - 0x0f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, - 0x12, 0x0a, 0x0a, 0x06, 0x4c, 0x45, 0x44, 0x47, 0x45, 0x52, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, - 0x54, 0x52, 0x49, 0x53, 0x41, 0x44, 0x53, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x52, 0x49, - 0x53, 0x41, 0x50, 0x32, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x42, 0x4c, 0x4f, 0x43, 0x4b, - 0x43, 0x48, 0x41, 0x49, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x10, 0x04, 0x32, 0x44, 0x0a, 0x09, 0x54, 0x52, 0x49, 0x53, 0x41, 0x44, 0x65, 0x6d, 0x6f, 0x12, - 0x37, 0x0a, 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x11, - 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x1a, 0x11, 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x28, 0x01, 0x30, 0x01, 0x32, 0x95, 0x01, 0x0a, 0x10, 0x54, 0x52, 0x49, - 0x53, 0x41, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, - 0x08, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x72, 0x76, 0x61, 0x73, - 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x41, 0x0a, - 0x0d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, - 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, - 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, - 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, - 0x72, 0x69, 0x73, 0x61, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x6e, - 0x65, 0x74, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2f, 0x70, 0x62, 0x2f, - 0x76, 0x31, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x4a, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x07, 0x12, + 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x08, 0x2a, 0x2b, + 0x0a, 0x03, 0x52, 0x50, 0x43, 0x12, 0x09, 0x0a, 0x05, 0x4e, 0x4f, 0x52, 0x50, 0x43, 0x10, 0x00, + 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x10, 0x01, 0x12, 0x0b, + 0x0a, 0x07, 0x41, 0x43, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x02, 0x2a, 0x53, 0x0a, 0x0f, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0a, + 0x0a, 0x06, 0x4c, 0x45, 0x44, 0x47, 0x45, 0x52, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x54, 0x52, + 0x49, 0x53, 0x41, 0x44, 0x53, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x52, 0x49, 0x53, 0x41, + 0x50, 0x32, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, + 0x41, 0x49, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, + 0x32, 0x44, 0x0a, 0x09, 0x54, 0x52, 0x49, 0x53, 0x41, 0x44, 0x65, 0x6d, 0x6f, 0x12, 0x37, 0x0a, + 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x11, 0x2e, 0x72, + 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, + 0x11, 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x28, 0x01, 0x30, 0x01, 0x32, 0x95, 0x01, 0x0a, 0x10, 0x54, 0x52, 0x49, 0x53, 0x41, + 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x08, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x41, 0x0a, 0x0d, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x2e, 0x72, + 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x34, + 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x69, + 0x73, 0x61, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x6e, 0x65, 0x74, + 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x72, 0x76, 0x61, 0x73, 0x70, 0x2f, 0x70, 0x62, 0x2f, 0x76, 0x31, + 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/rvasp/rvasp.go b/pkg/rvasp/rvasp.go index dfda3b5..bd4bb5c 100644 --- a/pkg/rvasp/rvasp.go +++ b/pkg/rvasp/rvasp.go @@ -166,10 +166,10 @@ func (s *Server) Shutdown() (err error) { // Transfer accepts a transfer request from a beneficiary and begins the InterVASP // protocol to perform identity verification prior to establishing the transaction in // the blockchain between crypto wallet addresses. -func (s *Server) Transfer(ctx context.Context, req *pb.TransferRequest) (*pb.TransferReply, error) { +func (s *Server) Transfer(ctx context.Context, req *pb.TransferRequest) (reply *pb.TransferReply, err error) { // Get originator account and confirm it belongs to this RVASP var account db.Account - if err := s.db.LookupAccount(req.Account).First(&account).Error; err != nil { + if err = s.db.LookupAccount(req.Account).First(&account).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { log.Info().Str("account", req.Account).Msg("not found") return nil, status.Error(codes.NotFound, "account not found") @@ -180,7 +180,7 @@ func (s *Server) Transfer(ctx context.Context, req *pb.TransferRequest) (*pb.Tra // Retrieve the policy for the originator account var wallet db.Wallet - if err := s.db.LookupWallet(account.WalletAddress).First(&wallet).Error; err != nil { + if err = s.db.LookupWallet(account.WalletAddress).First(&wallet).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { log.Info().Str("wallet", account.WalletAddress).Msg("not found") return nil, status.Error(codes.NotFound, "wallet not found") @@ -189,25 +189,60 @@ func (s *Server) Transfer(ctx context.Context, req *pb.TransferRequest) (*pb.Tra return nil, status.Errorf(codes.FailedPrecondition, "could not lookup wallet: %s", err) } + // Fetch the beneficiary Wallet + var beneficiary *db.Wallet + if beneficiary, err = s.fetchBeneficiaryWallet(req); err != nil { + return nil, err + } + + // Create a new Transaction + var xfer *db.Transaction + if xfer, err = s.db.MakeTransaction(account.WalletAddress, beneficiary.Address); err != nil { + return nil, err + } + xfer.Account = account + xfer.Amount = decimal.NewFromFloat32(req.Amount) + xfer.Debit = true + // Run the scenario for the wallet's configured policy + var transferError error policy := wallet.OriginatorPolicy log.Debug().Str("wallet", account.WalletAddress).Str("policy", string(policy)).Msg("initiating transfer") switch policy { case db.SendPartial: // Send a transfer request to the beneficiary containing partial beneficiary // identity information. - return s.sendTransfer(req, account, true) + transferError = s.sendTransfer(xfer, beneficiary, true) case db.SendFull: // Send a transfer request to the beneficiary containing full beneficiary // identity information. - return s.sendTransfer(req, account, false) + transferError = s.sendTransfer(xfer, beneficiary, false) case db.SendError: // Send a TRISA error to the beneficiary. - return s.sendError(req, account) + transferError = s.sendError(xfer, beneficiary) default: log.Error().Str("wallet", account.WalletAddress).Str("policy", string(policy)).Msg("unknown policy") return nil, status.Errorf(codes.FailedPrecondition, "unknown originator policy '%s' for wallet '%s'", policy, account.WalletAddress) } + + if transferError != nil { + log.Debug().Err(transferError).Msg("transfer failed") + xfer.SetState(pb.TransactionState_FAILED) + } + + // Return the transfer response + reply = &pb.TransferReply{ + Transaction: xfer.Proto(), + } + + // Save the updated transaction + // TODO: Clean up completed transactions in the database + if err = s.db.Save(xfer).Error; err != nil { + log.Error().Err(err).Msg("could not save transaction") + return nil, status.Errorf(codes.Internal, "could not save transaction: %s", err) + } + + return reply, transferError } // fetchBeneficiary fetches the beneficiary Wallet from the request. @@ -254,60 +289,45 @@ func (s *Server) fetchBeneficiaryWallet(req *pb.TransferRequest) (wallet *db.Wal // to the beneficiary. If partial is true, then the full beneficiary identity // information is not included in the payload. This function handles pending responses // from the beneficiary saving the transaction in an "await" state in the database. -func (s *Server) sendTransfer(req *pb.TransferRequest, account db.Account, partial bool) (rep *pb.TransferReply, err error) { - rep = &pb.TransferReply{} - - // Fetch the beneficiary Wallet - var beneficiary *db.Wallet - if beneficiary, err = s.fetchBeneficiaryWallet(req); err != nil { - return nil, err - } - +func (s *Server) sendTransfer(xfer *db.Transaction, beneficiary *db.Wallet, partial bool) (err error) { // Conduct a GDS lookup if necessary to get the endpoint var peer *peers.Peer if peer, err = s.peers.Search(beneficiary.Provider.Name); err != nil { log.Warn().Err(err).Msg("could not search peer from directory service") - return nil, status.Errorf(codes.FailedPrecondition, "could not search peer from directory service: %s", err) + return status.Errorf(codes.FailedPrecondition, "could not search peer from directory service: %s", err) } // Ensure that the local RVASP has signing keys for the remote, otherwise perform key exchange var signKey *rsa.PublicKey if signKey, err = peer.ExchangeKeys(true); err != nil { log.Warn().Err(err).Msg("could not exchange keys with remote peer") - return nil, status.Errorf(codes.FailedPrecondition, "could not exchange keys with remote peer: %s", err) + return status.Errorf(codes.FailedPrecondition, "could not exchange keys with remote peer: %s", err) } - // Create a new Transaction in the database - var xfer db.Transaction - if xfer, err = s.db.MakeTransaction(account.WalletAddress, beneficiary.Address); err != nil { - return nil, err - } - xfer.Account = account - xfer.Amount = decimal.NewFromFloat32(req.Amount) - xfer.Debit = true - - if err = s.db.Create(&xfer).Error; err != nil { + if err = s.db.Create(xfer).Error; err != nil { log.Error().Err(err).Msg("could not save pending transaction") - return nil, status.Errorf(codes.FailedPrecondition, "could not save pending transaction: %s", err) + return status.Errorf(codes.FailedPrecondition, "could not save pending transaction: %s", err) } // Save the pending transaction on the account // TODO: remove pending transactions - account.Pending++ - if err = s.db.Save(&account).Error; err != nil { + xfer.Account.Pending++ + if err = s.db.Save(&xfer.Account).Error; err != nil { log.Error().Err(err).Msg("could not save originator account") - return nil, status.Errorf(codes.FailedPrecondition, "could not save originator account: %s", err) + return status.Errorf(codes.FailedPrecondition, "could not save originator account: %s", err) } // Create an identity and transaction payload for TRISA exchange transaction := &generic.Transaction{ - Originator: account.WalletAddress, + Originator: xfer.Account.WalletAddress, Beneficiary: beneficiary.Address, - Amount: float64(req.Amount), Network: "TestNet", Timestamp: xfer.Timestamp.Format(time.RFC3339), } + // Set the amount on the transaction payload + transaction.Amount, _ = xfer.Amount.Float64() + var beneficiaryAccount db.Account if partial { // If partial is specified then only populate the benefiiary address @@ -318,53 +338,64 @@ func (s *Server) sendTransfer(req *pb.TransferRequest, account db.Account, parti // If partial is false then retrieve the full beneficiary account if err = s.db.LookupAnyAccount(beneficiary.Address).First(&beneficiaryAccount).Error; err != nil { log.Warn().Err(err).Msg("could not lookup remote beneficiary account") - return nil, status.Errorf(codes.FailedPrecondition, "could not lookup remote beneficiary account: %s", err) + return status.Errorf(codes.FailedPrecondition, "could not lookup remote beneficiary account: %s", err) } } var identity *ivms101.IdentityPayload - if identity, err = s.createIdentityPayload(account, beneficiaryAccount); err != nil { - return nil, err + if identity, err = s.createIdentityPayload(xfer.Account, beneficiaryAccount); err != nil { + return err } var payload *protocol.Payload if payload, err = createTransferPayload(identity, transaction); err != nil { log.Error().Err(err).Msg("could not create transfer payload") - return nil, status.Errorf(codes.Internal, "could not create transfer payload: %s", err) + return status.Errorf(codes.Internal, "could not create transfer payload: %s", err) } // Secure the envelope with the remote beneficiary's signing keys msg, _, err := envelope.Seal(payload, envelope.WithEnvelopeID(xfer.Envelope), envelope.WithRSAPublicKey(signKey)) if err != nil { log.Warn().Err(err).Msg("TRISA protocol error while sealing envelope") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) } // Conduct the TRISA transaction, handle errors and send back to user if msg, err = peer.Transfer(msg); err != nil { log.Warn().Err(err).Msg("could not perform TRISA exchange") - return nil, status.Errorf(codes.FailedPrecondition, "could not perform TRISA exchange: %s", err) + return status.Errorf(codes.FailedPrecondition, "could not perform TRISA exchange: %s", err) + } + + // Check for TRISA rejection errors + reject, isErr := envelope.Check(msg) + if isErr { + if reject != nil { + xfer.SetState(pb.TransactionState_REJECTED) + return nil + } + log.Warn().Err(err).Msg("TRISA protocol error while checking envelope") + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) } // Open the response envelope with local private keys payload, _, err = envelope.Open(msg, envelope.WithRSAPrivateKey(s.trisa.sign)) if err != nil { log.Warn().Err(err).Msg("TRISA protocol error while opening envelope") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) } // Parse the response payload var pending *generic.Pending if identity, transaction, pending, err = parsePayload(payload, true); err != nil { log.Warn().Err(err).Msg("TRISA protocol error while parsing payload") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) } // Update the transaction with the identity information var data []byte if data, err = json.Marshal(identity); err != nil { log.Error().Err(err).Msg("could not marshal IVMS 101 identity") - return nil, status.Errorf(codes.Internal, "could not marshal IVMS 101 identity: %s", err) + return status.Errorf(codes.Internal, "could not marshal IVMS 101 identity: %s", err) } xfer.Identity = string(data) @@ -373,134 +404,99 @@ func (s *Server) sendTransfer(req *pb.TransferRequest, account db.Account, parti // Update the Transaction in the database with the pending timestamps if xfer.NotBefore, err = time.Parse(time.RFC3339, pending.ReplyNotBefore); err != nil { log.Warn().Err(err).Msg("TRISA protocol error: could not parse ReplyNotBefore timestamp") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: could not parse ReplyNotBefore timestamp in pending message: %s", err) + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: could not parse ReplyNotBefore timestamp in pending message: %s", err) } if xfer.NotAfter, err = time.Parse(time.RFC3339, pending.ReplyNotAfter); err != nil { log.Warn().Err(err).Msg("TRISA protocol error: could not parse ReplyNotAfter timestamp") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: could not parse ReplyNotAfter timestamp in pending message: %s", err) + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: could not parse ReplyNotAfter timestamp in pending message: %s", err) } } else if transaction != nil { if !partial { // Validate that the beneficiary identity matches the original request if identity.BeneficiaryVasp == nil || identity.BeneficiaryVasp.BeneficiaryVasp == nil { log.Warn().Msg("TRISA protocol error: missing beneficiary vasp identity") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: missing beneficiary vasp identity") + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: missing beneficiary vasp identity") } beneficiaryInfo := identity.Beneficiary if beneficiaryInfo == nil { log.Warn().Msg("TRISA protocol error: missing beneficiary identity") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: missing beneficiary identity") + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: missing beneficiary identity") } if len(beneficiaryInfo.BeneficiaryPersons) != 1 { log.Warn().Int("persons", len(beneficiaryInfo.BeneficiaryPersons)).Msg("TRISA protocol error: unexpected number of beneficiary persons") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: unexpected number of beneficiary persons") + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: unexpected number of beneficiary persons") } if len(beneficiaryInfo.AccountNumbers) != 1 { log.Warn().Int("accounts", len(beneficiaryInfo.AccountNumbers)).Msg("TRISA protocol error: unexpected number of beneficiary accounts") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: unexpected number of beneficiary accounts") + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: unexpected number of beneficiary accounts") } // TODO: Validate that the actual address was returned if beneficiaryInfo.AccountNumbers[0] == "" { log.Warn().Msg("TRISA protocol error: missing beneficiary address") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: missing beneficiary address") + return status.Errorf(codes.FailedPrecondition, "TRISA protocol error: missing beneficiary address") } } // Update the account information - account.Pending-- - account.Completed++ - account.Balance = account.Balance.Sub(xfer.Amount) - if err = s.db.Save(&account).Error; err != nil { + xfer.Account.Pending-- + xfer.Account.Completed++ + xfer.Account.Balance = xfer.Account.Balance.Sub(xfer.Amount) + if err = s.db.Save(xfer.Account).Error; err != nil { log.Error().Err(err).Msg("could not save originator account") - return nil, status.Errorf(codes.Internal, "could not save originator account: %s", err) + return status.Errorf(codes.Internal, "could not save originator account: %s", err) } // This transaction is now complete - xfer.State = pb.TransactionState_COMPLETED + xfer.SetState(pb.TransactionState_COMPLETED) xfer.Timestamp, _ = time.Parse(time.RFC3339, transaction.Timestamp) } - // Save the updated transaction - // TODO: Clean up completed transactions in the database - if err = s.db.Save(&xfer).Error; err != nil { - log.Error().Err(err).Msg("could not save transaction") - return nil, status.Errorf(codes.Internal, "could not save transaction: %s", err) - } - - // Return the transfer response - rep.Transaction = xfer.Proto() - rep.Transaction.State = xfer.State - return rep, nil + return nil } // sendError sends a TRISA error to the beneficiary. -func (s *Server) sendError(req *pb.TransferRequest, account db.Account) (rep *pb.TransferReply, err error) { - rep = &pb.TransferReply{} - - // Fetch the beneficiary Wallet - var beneficiary *db.Wallet - if beneficiary, err = s.fetchBeneficiaryWallet(req); err != nil { - return nil, err - } - - // Create a new Transaction - var xfer db.Transaction - if xfer, err = s.db.MakeTransaction(account.WalletAddress, beneficiary.Address); err != nil { - return nil, err - } - xfer.Account = account - xfer.Amount = decimal.NewFromFloat32(req.Amount) - xfer.Debit = true - +func (s *Server) sendError(xfer *db.Transaction, beneficiary *db.Wallet) (err error) { // Conduct a GDS lookup if necessary to get the endpoint var peer *peers.Peer if peer, err = s.peers.Search(beneficiary.Provider.Name); err != nil { log.Warn().Err(err).Msg("could not search peer from directory service") - return nil, status.Errorf(codes.FailedPrecondition, "could not search peer from directory service: %s", err) + return status.Errorf(codes.FailedPrecondition, "could not search peer from directory service: %s", err) } reject := protocol.Errorf(protocol.ComplianceCheckFail, "rVASP mock compliance check failed") var msg *protocol.SecureEnvelope if msg, err = envelope.Reject(reject); err != nil { log.Error().Err(err).Msg("could not create TRISA error envelope") - return nil, status.Errorf(codes.Internal, "could not create TRISA error envelope: %s", err) + return status.Errorf(codes.Internal, "could not create TRISA error envelope: %s", err) } // Conduct the TRISA transaction, handle errors and send back to user if msg, err = peer.Transfer(msg); err != nil { log.Warn().Err(err).Msg("could not perform TRISA exchange") - return nil, status.Errorf(codes.FailedPrecondition, "could not perform TRISA exchange: %s", err) + return status.Errorf(codes.FailedPrecondition, "could not perform TRISA exchange: %s", err) } // Check for the TRISA rejection error if state := envelope.Status(msg); state != envelope.Error { log.Warn().Uint("state", uint(state)).Msg("unexpected TRISA response, expected error envelope") - return nil, fmt.Errorf("expected TRISA rejection error, received envelope in state %d", state) + return fmt.Errorf("expected TRISA rejection error, received envelope in state %d", state) } - xfer.State = pb.TransactionState_FAILED + xfer.SetState(pb.TransactionState_REJECTED) - rep.Transaction = xfer.Proto() - return rep, nil + return nil } // respondAsync responds to a serviced transfer request from the beneficiary by // continuing or completing the asynchronous handshake. -func (s *Server) respondAsync(peer *peers.Peer, payload *protocol.Payload, identity *ivms101.IdentityPayload, transaction *generic.Transaction, envelopeID string) (out *protocol.SecureEnvelope, err error) { +func (s *Server) respondAsync(peer *peers.Peer, payload *protocol.Payload, identity *ivms101.IdentityPayload, transaction *generic.Transaction, xfer *db.Transaction) (out *protocol.SecureEnvelope, err error) { // Secure envelope was successfully received now := time.Now() - // Lookup the pending transaction in the database - var xfer db.Transaction - if err = s.db.LookupTransaction(envelopeID).First(&xfer).Error; err != nil { - log.Error().Err(err).Msg("could not find pending transaction") - return nil, protocol.Errorf(protocol.InternalError, "could not find pending transaction: %s", err) - } - // Verify that the transaction has not expired if now.Before(xfer.NotBefore) || now.After(xfer.NotAfter) { log.Debug().Time("not_before", xfer.NotBefore).Time("not_after", xfer.NotAfter).Msg("received expired async transaction") @@ -514,6 +510,7 @@ func (s *Server) respondAsync(peer *peers.Peer, payload *protocol.Payload, ident if out, err = envelope.Reject(reject); err != nil { return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) } + xfer.SetState(pb.TransactionState_REJECTED) return out, nil } log.Warn().Err(err).Msg("TRISA protocol error while sealing envelope") @@ -595,7 +592,7 @@ func (s *Server) respondAsync(peer *peers.Peer, payload *protocol.Payload, ident return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: could not parse ReplyNotAfter timestamp in pending message: %s", err) } - xfer.State = pb.TransactionState_ACCEPTED + xfer.SetState(pb.TransactionState_ACCEPTED) case pb.TransactionState_ACCEPTED: // The handshake is complete, finalize the transaction var account db.Account @@ -613,18 +610,12 @@ func (s *Server) respondAsync(peer *peers.Peer, payload *protocol.Payload, ident log.Error().Err(err).Msg("could not save originator account") return nil, status.Errorf(codes.Internal, "could not save originator account: %s", err) } - xfer.State = pb.TransactionState_COMPLETED + xfer.SetState(pb.TransactionState_COMPLETED) default: log.Error().Str("state", xfer.State.String()).Msg("unepected transaction state") return nil, protocol.Errorf(protocol.ComplianceCheckFail, "unexpected transaction state: %s", xfer.State.String()) } - // Save the updated transaction - if err = s.db.Save(&xfer).Error; err != nil { - log.Error().Err(err).Msg("could not save transaction") - return nil, status.Errorf(codes.Internal, "could not save transaction: %s", err) - } - return out, nil } @@ -994,7 +985,7 @@ func (s *Server) handleTransaction(client string, req *pb.Command) (err error) { WalletAddress: transaction.Beneficiary, Vasp: s.vasp, } - xfer.State = pb.TransactionState_COMPLETED + xfer.SetState(pb.TransactionState_COMPLETED) xfer.Timestamp, _ = time.Parse(time.RFC3339, transaction.Timestamp) // Serialize the identity information as JSON data diff --git a/pkg/rvasp/testdata/ca.gz b/pkg/rvasp/testdata/ca.gz new file mode 100644 index 0000000..efd0914 Binary files /dev/null and b/pkg/rvasp/testdata/ca.gz differ diff --git a/pkg/rvasp/trisa.go b/pkg/rvasp/trisa.go index ace0b5f..4cd33e9 100644 --- a/pkg/rvasp/trisa.go +++ b/pkg/rvasp/trisa.go @@ -251,6 +251,24 @@ func (s *TRISA) handleTransaction(ctx context.Context, peer *peers.Peer, in *pro if out, err = envelope.Reject(reject, envelope.WithEnvelopeID(in.Id)); err != nil { return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) } + + // Lookup the indicated transaction in the database + xfer := &db.Transaction{} + if err = s.parent.db.LookupTransaction(in.Id).First(xfer).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Warn().Err(err).Str("id", in.Id).Msg("transaction not found") + } else { + log.Error().Err(err).Msg("could not perform transaction lookup") + } + return out, nil + } + + // Set the transaction state to rejected + xfer.SetState(pb.TransactionState_REJECTED) + if err = s.parent.db.Save(xfer).Error; err != nil { + log.Error().Err(err).Msg("could not save transaction") + return nil, status.Errorf(codes.Internal, "could not save transaction: %s", err) + } return out, nil } log.Warn().Err(err).Msg("TRISA protocol error while checking envelope") @@ -304,7 +322,28 @@ func (s *TRISA) handleTransaction(ctx context.Context, peer *peers.Peer, in *pro log.Warn().Err(err).Msg("could not lookup remote peer") return nil, protocol.Errorf(protocol.InternalError, "could not lookup remote peer") } - return s.parent.respondAsync(peer, payload, identity, transaction, in.Id) + + // Lookup the pending transaction in the database + xfer := &db.Transaction{} + if err = s.parent.db.LookupTransaction(in.Id).First(xfer).Error; err != nil { + log.Error().Err(err).Msg("could not find pending transaction") + return nil, protocol.Errorf(protocol.InternalError, "could not find pending transaction: %s", err) + } + + // Perform the transfer back to the originator + var transferErr error + if out, transferErr = s.parent.respondAsync(peer, payload, identity, transaction, xfer); transferErr != nil { + log.Warn().Err(err).Msg("TRISA protocol error while responding to async transaction") + xfer.SetState(pb.TransactionState_FAILED) + } + + // Save the updated transaction + if err = s.parent.db.Save(xfer).Error; err != nil { + log.Error().Err(err).Msg("could not save transaction") + return nil, status.Errorf(codes.Internal, "could not save transaction: %s", err) + } + + return out, transferErr } // Lookup the beneficiary in the local VASP database. @@ -337,29 +376,68 @@ func (s *TRISA) handleTransaction(ctx context.Context, peer *peers.Peer, in *pro return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") } + // Fetch or create the transaction from the envelope ID + xfer := &db.Transaction{} + if err = s.parent.db.LookupTransaction(in.Id).First(xfer).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Create a new pending transaction in the database + if xfer, err = s.parent.db.MakeTransaction(transaction.Originator, transaction.Beneficiary); err != nil { + log.Error().Err(err).Msg("could not construct transaction") + return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") + } + xfer.Envelope = in.Id + xfer.Account = account + xfer.Amount = decimal.NewFromFloat(transaction.Amount) + xfer.Debit = false + + if err = s.parent.db.Create(xfer).Error; err != nil { + log.Error().Err(err).Msg("could not create transaction in database") + return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") + } + } else { + log.Error().Err(err).Msg("could not perform transaction lookup") + return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") + } + } + // Run the scenario for the wallet's configured policy + var transferError error policy := wallet.BeneficiaryPolicy log.Debug().Str("wallet", account.WalletAddress).Str("policy", string(policy)).Msg("received transfer request") switch policy { case db.SyncRepair: // Respond to the transfer request immediately, filling in the beneficiary // identity information. - return s.respondTransfer(in, peer, identity, transaction, account, false) + out, transferError = s.respondTransfer(in, peer, identity, transaction, xfer, account, false) case db.SyncRequire: // Respond to the transfer request immediately, requiring that the beneficiary // identity is already filled in. - return s.respondTransfer(in, peer, identity, transaction, account, true) + out, transferError = s.respondTransfer(in, peer, identity, transaction, xfer, account, true) case db.AsyncRepair: // Respond to the transfer request with a pending message and mark the // transaction for later service. The beneficiary information is filled in. - return s.respondPending(in, peer, identity, transaction, account) + out, transferError = s.respondPending(in, peer, identity, transaction, xfer, account) case db.AsyncReject: // Respond to the transfer request with a pending message that will be later // rejected. - return s.respondPending(in, peer, identity, transaction, account) + out, transferError = s.respondPending(in, peer, identity, transaction, xfer, account) default: return nil, protocol.Errorf(protocol.InternalError, "unknown policy '%s' for wallet '%s'", policy, account.WalletAddress) } + + if transferError != nil { + log.Debug().Err(transferError).Msg("transfer failed") + xfer.SetState(pb.TransactionState_FAILED) + } + + // Save the updated transaction + // TODO: Clean up completed transactions in the database + if err = s.parent.db.Save(xfer).Error; err != nil { + log.Error().Err(err).Msg("could not save transaction") + return nil, protocol.Errorf(protocol.InternalError, "could not save transaction: %s", err) + } + + return out, transferError } // Repair the identity payload in a received transfer request by filling in the @@ -389,7 +467,7 @@ func (s *TRISA) repairBeneficiary(identity *ivms101.IdentityPayload, account db. // the payload with the beneficiary identity information. If requireBeneficiary is // true, the beneficiary identity must be filled in, or the transfer is rejected. If // requireBeneficiary is false, the partial beneficiary identity is repaired. -func (s *TRISA) respondTransfer(in *protocol.SecureEnvelope, peer *peers.Peer, identity *ivms101.IdentityPayload, transaction *generic.Transaction, account db.Account, requireBeneficiary bool) (out *protocol.SecureEnvelope, err error) { +func (s *TRISA) respondTransfer(in *protocol.SecureEnvelope, peer *peers.Peer, identity *ivms101.IdentityPayload, transaction *generic.Transaction, xfer *db.Transaction, account db.Account, requireBeneficiary bool) (out *protocol.SecureEnvelope, err error) { // Save the pending transaction on the account account.Pending++ if err = s.parent.db.Save(&account).Error; err != nil { @@ -401,7 +479,13 @@ func (s *TRISA) respondTransfer(in *protocol.SecureEnvelope, peer *peers.Peer, i // TODO: Validate the actual fields in the beneficiary identity if identity.BeneficiaryVasp == nil || identity.BeneficiaryVasp.BeneficiaryVasp == nil { log.Warn().Msg("TRISA protocol error: missing beneficiary vasp identity") - return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: missing beneficiary vasp identity") + reject := protocol.Errorf(protocol.Rejected, "missing beneficiary vasp identity") + if out, err = envelope.Reject(reject, envelope.WithEnvelopeID(in.Id)); err != nil { + log.Error().Err(err).Msg("could not create reject envelope") + return nil, protocol.Errorf(protocol.InternalError, "request coould not be processed: %s", err) + } + xfer.SetState(pb.TransactionState_REJECTED) + return out, nil } } else { // Fill in the beneficiary identity information for the repair policy @@ -412,29 +496,12 @@ func (s *TRISA) respondTransfer(in *protocol.SecureEnvelope, peer *peers.Peer, i transaction.Beneficiary = account.WalletAddress transaction.Timestamp = time.Now().Format(time.RFC3339) - // Save the completed transaction in the database - var ach db.Transaction - if ach, err = s.parent.db.MakeTransaction(transaction.Originator, transaction.Beneficiary); err != nil { - log.Error().Err(err).Msg("could not create transaction") - return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") - } - ach.Envelope = in.Id - ach.Account = account - ach.Amount = decimal.NewFromFloat(transaction.Amount) - ach.Debit = false - ach.State = pb.TransactionState_COMPLETED - - var achBytes []byte - if achBytes, err = protojson.Marshal(identity); err != nil { + var xferBytes []byte + if xferBytes, err = protojson.Marshal(identity); err != nil { log.Error().Err(err).Msg("could not marshal transaction identity") return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") } - ach.Identity = string(achBytes) - - if err = s.parent.db.Create(&ach).Error; err != nil { - log.Error().Err(err).Msg("could not create transaction in database") - return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") - } + xfer.Identity = string(xferBytes) // Update the account information account.Balance = account.Balance.Add(decimal.NewFromFloat(transaction.Amount)) @@ -445,7 +512,7 @@ func (s *TRISA) respondTransfer(in *protocol.SecureEnvelope, peer *peers.Peer, i return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") } - msg := fmt.Sprintf("ready for transaction %04d: %s transfering from %s to %s", ach.ID, ach.Amount, ach.Originator.WalletAddress, ach.Beneficiary.WalletAddress) + msg := fmt.Sprintf("ready for transaction %04d: %s transfering from %s to %s", xfer.ID, xfer.Amount, xfer.Originator.WalletAddress, xfer.Beneficiary.WalletAddress) s.parent.updates.Broadcast(0, msg, pb.MessageCategory_BLOCKCHAIN) // Encode and encrypt the payload information to return the secure envelope @@ -470,6 +537,7 @@ func (s *TRISA) respondTransfer(in *protocol.SecureEnvelope, peer *peers.Peer, i if out, err = envelope.Reject(reject, envelope.WithEnvelopeID(in.Id)); err != nil { return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) } + xfer.SetState(pb.TransactionState_REJECTED) return out, nil } log.Warn().Err(err).Msg("TRISA protocol error while sealing envelope") @@ -477,40 +545,23 @@ func (s *TRISA) respondTransfer(in *protocol.SecureEnvelope, peer *peers.Peer, i } s.parent.updates.Broadcast(0, fmt.Sprintf("%04d new account balance: %s", account.ID, account.Balance), pb.MessageCategory_LEDGER) + + // Mark transaction as completed + xfer.SetState(pb.TransactionState_COMPLETED) + return out, nil } // respondPending responds to a transfer request from the originator by returning a // pending message and saving the pending transaction in the database. -func (s *TRISA) respondPending(in *protocol.SecureEnvelope, peer *peers.Peer, identity *ivms101.IdentityPayload, transaction *generic.Transaction, account db.Account) (out *protocol.SecureEnvelope, err error) { +func (s *TRISA) respondPending(in *protocol.SecureEnvelope, peer *peers.Peer, identity *ivms101.IdentityPayload, transaction *generic.Transaction, xfer *db.Transaction, account db.Account) (out *protocol.SecureEnvelope, err error) { now := time.Now() - // Check if the pending transaction exists, otherwise create it - var xfer db.Transaction - if err = s.parent.db.LookupTransaction(in.Id).First(&xfer).Error; err != nil { - if err == gorm.ErrRecordNotFound { - // Create a new pending transaction in the database - if xfer, err = s.parent.db.MakeTransaction(transaction.Originator, transaction.Beneficiary); err != nil { - log.Error().Err(err).Msg("could not construct transaction") - return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") - } - xfer.Envelope = in.Id - xfer.Account = account - xfer.Amount = decimal.NewFromFloat(transaction.Amount) - xfer.Debit = false - xfer.State = pb.TransactionState_PENDING_SENT - - if err = s.parent.db.Create(&xfer).Error; err != nil { - log.Error().Err(err).Msg("could not create transaction in database") - return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") - } - } else { - log.Error().Err(err).Msg("could not perform transaction lookup") - return nil, protocol.Errorf(protocol.InternalError, "request could not be processed") - } + // Update the transaction state + if xfer.State == pb.TransactionState_AWAITING { + xfer.SetState(pb.TransactionState_PENDING_SENT) } else { - // Update the existing pending transaction - xfer.State = pb.TransactionState_PENDING_ACKNOWLEDGED + xfer.SetState(pb.TransactionState_PENDING_ACKNOWLEDGED) } xfer.NotBefore = now.Add(s.parent.conf.AsyncNotBefore) @@ -576,6 +627,7 @@ func (s *TRISA) respondPending(in *protocol.SecureEnvelope, peer *peers.Peer, id if out, err = envelope.Reject(reject, envelope.WithEnvelopeID(in.Id)); err != nil { return nil, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: %s", err) } + xfer.SetState(pb.TransactionState_REJECTED) return out, nil } log.Warn().Err(err).Msg("TRISA protocol error while sealing envelope") @@ -686,7 +738,7 @@ func (s *TRISA) sendAsync(tx *db.Transaction) (err error) { switch tx.State { case pb.TransactionState_PENDING_SENT: // The first handshake is complete so move the transaction to the next state - tx.State = pb.TransactionState_PENDING_ACKNOWLEDGED + tx.SetState(pb.TransactionState_PENDING_ACKNOWLEDGED) case pb.TransactionState_PENDING_ACKNOWLEDGED: // This is a complete transaction so update the database var account *db.Account @@ -705,7 +757,7 @@ func (s *TRISA) sendAsync(tx *db.Transaction) (err error) { msg := fmt.Sprintf("ready for transaction %s: %.2f transfering from %s to %s", transaction.Txid, transaction.Amount, transaction.Originator, transaction.Beneficiary) s.parent.updates.Broadcast(0, msg, pb.MessageCategory_BLOCKCHAIN) - tx.State = pb.TransactionState_COMPLETED + tx.SetState(pb.TransactionState_COMPLETED) default: log.Error().Str("state", tx.State.String()).Msg("unexpected transaction state") return fmt.Errorf("unexpected transaction state: %s", tx.State.String()) @@ -760,7 +812,7 @@ func (s *TRISA) sendRejected(tx *db.Transaction) (err error) { return fmt.Errorf("expected TRISA rejection error, received envelope in state %d", state) } - tx.State = pb.TransactionState_FAILED + tx.SetState(pb.TransactionState_REJECTED) return nil } diff --git a/pkg/rvasp/trisa_test.go b/pkg/rvasp/trisa_test.go index 9b52d73..85417af 100644 --- a/pkg/rvasp/trisa_test.go +++ b/pkg/rvasp/trisa_test.go @@ -18,8 +18,6 @@ import ( "github.com/trisacrypto/trisa/pkg/trisa/mtls" "github.com/trisacrypto/trisa/pkg/trisa/peers" "github.com/trisacrypto/trisa/pkg/trust" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/anypb" ) @@ -112,29 +110,20 @@ func (s *rVASPTestSuite) TestValidTransfer() { s.db.ExpectQuery("SELECT").WillReturnRows(sqlmock.NewRows([]string{"wallet_address"}).AddRow(beneficiaryAddress)) s.db.ExpectQuery("SELECT").WillReturnRows(sqlmock.NewRows([]string{"beneficiary_policy"}).AddRow("SyncRequire")) + // Preload the transaction lookup + expectStandardQuery(s.db, "SELECT") + // Preload the beneficiary account insert s.db.ExpectBegin() expectStandardQuery(s.db, "INSERT") s.db.ExpectCommit() - // Preload the identity lookups - expectStandardQuery(s.db, "SELECT") - expectStandardQuery(s.db, "SELECT") - - // Preload the transaction insert + // Account record update s.db.ExpectBegin() - // Account record insert - expectStandardQuery(s.db, "INSERT") - // Identity records insert - expectStandardQuery(s.db, "INSERT") - expectStandardQuery(s.db, "INSERT") - // VASP record insert - expectStandardQuery(s.db, "INSERT") - // Transaction record insert - expectStandardQuery(s.db, "INSERT") + s.db.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(1, 1)) s.db.ExpectCommit() - // Account record update + // Transaction record update s.db.ExpectBegin() s.db.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(1, 1)) s.db.ExpectCommit() @@ -213,11 +202,19 @@ func (s *rVASPTestSuite) TestInvalidTransfer() { s.db.ExpectQuery("SELECT").WillReturnRows(sqlmock.NewRows([]string{"wallet_address"}).AddRow(beneficiaryAddress)) s.db.ExpectQuery("SELECT").WillReturnRows(sqlmock.NewRows([]string{"beneficiary_policy"}).AddRow("SyncRequire")) + // Preload the transaction lookup + expectStandardQuery(s.db, "SELECT") + // Preload the beneficiary account insert s.db.ExpectBegin() expectStandardQuery(s.db, "INSERT") s.db.ExpectCommit() + // Transaction record update + s.db.ExpectBegin() + s.db.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(1, 1)) + s.db.ExpectCommit() + // Seal the envelope using the public key key, err := s.certs.GetRSAKeys() require.NoError(err) @@ -233,6 +230,10 @@ func (s *rVASPTestSuite) TestInvalidTransfer() { client := protocol.NewTRISANetworkClient(s.grpc.Conn) // Do the request - _, err = client.Transfer(context.Background(), msg) - require.EqualError(err, status.Errorf(codes.FailedPrecondition, "TRISA protocol error: missing beneficiary vasp identity").Error()) + response, err := client.Transfer(context.Background(), msg) + require.NoError(err) + require.NotNil(response) + + // Should get a rejection error + require.Equal(envelope.Error, envelope.Status(response)) } diff --git a/proto/rvasp/v1/api.proto b/proto/rvasp/v1/api.proto index d8b14a6..c138c26 100644 --- a/proto/rvasp/v1/api.proto +++ b/proto/rvasp/v1/api.proto @@ -58,7 +58,8 @@ enum TransactionState { ACCEPTED = 4; // (Async) Originator has received the transaction acknowledgement from the beneficiary FAILED = 5; // The transaction has failed EXPIRED = 6; // The asynchronous transaction has expired before completion - COMPLETED = 7; // The transaction is completed + REJECTED = 7; // The transaction has been rejected + COMPLETED = 8; // The transaction is completed } // Initiates a transfer from the specified account to the specified wallet address or