From d3c7944ee2044bcab7b8db1c80b446ec509449ec Mon Sep 17 00:00:00 2001 From: AustEcon Date: Thu, 28 Jul 2022 20:37:03 +1200 Subject: [PATCH 1/8] Updates to `PaymentRequest` stuctures to align with the latest TSC spec. --- destinations.go | 31 +++--------------- merchant.go | 2 ++ payment_request.go | 80 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/destinations.go b/destinations.go index 7e937d4..a9ff527 100644 --- a/destinations.go +++ b/destinations.go @@ -1,35 +1,14 @@ package dpp import ( - "time" - - "github.com/libsv/go-bt/v2" "github.com/libsv/go-bt/v2/bscript" ) -// Output message used in BIP270. -// See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#output -type Output struct { +type DPPNativeOutput struct { // Amount is the number of satoshis to be paid. - Amount uint64 `json:"amount" example:"100000"` + Amount uint64 `json:"amount" binding:"required" example:"100000"` // Script is a locking script where payment should be sent, formatted as a hexadecimal string. - LockingScript *bscript.Script `json:"script" swaggertype:"primitive,string" example:"76a91455b61be43392125d127f1780fb038437cd67ef9c88ac"` + LockingScript *bscript.Script `json:"script" binding:"required" swaggertype:"primitive,string" example:"76a91455b61be43392125d127f1780fb038437cd67ef9c88ac"` // Description, an optional description such as "tip" or "sales tax". Maximum length is 100 chars. - Description string `json:"description" example:"paymentReference 123456"` -} - -// PaymentDestinations contains the supported destinations -// by this DPP server. -type PaymentDestinations struct { - Outputs []Output `json:"outputs"` -} - -// Destinations message containing outputs and their fees. -type Destinations struct { - AncestryRequired bool `json:"ancestryRequired"` - Network string `json:"network"` - Outputs []Output `json:"outputs"` - Fees *bt.FeeQuote `json:"fees"` - CreatedAt time.Time `json:"createdAt"` - ExpiresAt time.Time `json:"expiresAt"` -} + Description string `json:"description,omitempty" example:"paymentReference 123456"` +} \ No newline at end of file diff --git a/merchant.go b/merchant.go index 26deb2c..91c0413 100644 --- a/merchant.go +++ b/merchant.go @@ -12,4 +12,6 @@ type Merchant struct { Address string `json:"address" example:"1 the street, the town, B1 1AA"` // ExtendedData can be supplied if the merchant wishes to send some arbitrary data back to the wallet. ExtendedData map[string]interface{} `json:"extendedData,omitempty"` + // PaymentReference ID of invoice. + PaymentReference string `json:"paymentReference" example:"Order-325214"` } diff --git a/payment_request.go b/payment_request.go index d016a86..f252ff4 100644 --- a/payment_request.go +++ b/payment_request.go @@ -2,45 +2,83 @@ package dpp import ( "context" - "time" - - "github.com/libsv/go-bt/v2" ) -// PaymentRequest message used in BIP270. -// See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#paymentrequest +// These structures are defined in the TSC spec: +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + + +type DPPPolicies struct { + // FeeRate defines the amount of fees a users wallet should add to the payment + // when submitting their final payments. + FeeRate map[string]map[string]int `json:"fees,omitempty"` + SPVRequired bool `json:"SPVRequired,omitempty"` + LockTime uint32 `json:"lockTime,omitempty"` +} + +type DPPInput struct { + ScriptSig string `json:"scriptSig" binding:"required"`// string. required. + TxId string `json:"txid" binding:"required"`// string. required. + Vout uint32 `json:"vout" binding:"required"`// integer. required. + Value uint64 `json:"value" binding:"required"`// integer. required. + NSequence int `json:"nSequence,omitempty"` // number. optional. +} + + +type DPPOutputs struct { + NativeOutputs []DPPNativeOutput `json:"native"` +} + +// TransactionTerms a single definition of requested transaction format for the standard payment mode: +// "ef63d9775da5" in the DPP TSC spec. +type TransactionTerms struct { + Outputs DPPOutputs `json:"outputs"` + Inputs []DPPInput `json:"inputs,omitempty"` + Policies *DPPPolicies `json:"policies"` +} + +// PaymentModes message used in DPP TSC spec. +// At present we will strictly only allow the "standard" mode of payment with native bitcoins (satoshis). Handling +// of tokens is left for a later date. +type PaymentModes struct { + HybridPaymentMode map[string]map[string][]TransactionTerms `json:"ef63d9775da5"` +} + +// PaymentRequest message as defined in the DPP T$C spec. type PaymentRequest struct { // Network Always set to "bitcoin" (but seems to be set to 'bitcoin-sv' // outside bip270 spec, see https://handcash.github.io/handcash-merchant-integration/#/merchant-payments) // {enum: bitcoin, bitcoin-sv, test} // Required. - Network string `json:"network" example:"mainnet" enums:"mainnet,testnet,stn,regtest"` - // AncestryRequired if true will expect the sender to submit an ancestry in the payment request, otherwise - // a rawTx will be required. - AncestryRequired bool `json:"ancestryRequired" example:"true"` - // Destinations contains supported payment destinations by the merchant and dpp server, initial P2PKH outputs but can be extended. + Network string `json:"network" binding:"required" example:"mainnet" enums:"mainnet,testnet,stn,regtest"` + // Version version of DPP TSC spec. // Required. - Destinations PaymentDestinations `json:"destinations"` + Version string `json:"version" binding:"required" example:"1.0"` + // Outputs an array of outputs. DEPRECATED but included for backward compatibility. + // Optional. + Outputs []DPPNativeOutput `json:"outputs,omitempty"` // CreationTimestamp Unix timestamp (seconds since 1-Jan-1970 UTC) when the PaymentRequest was created. // Required. - CreationTimestamp time.Time `json:"creationTimestamp" swaggertype:"primitive,string" example:"2019-10-12T07:20:50.52Z"` + CreationTimestamp int64 `json:"creationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648163657"` // ExpirationTimestamp Unix timestamp (UTC) after which the PaymentRequest should be considered invalid. // Optional. - ExpirationTimestamp time.Time `json:"expirationTimestamp" swaggertype:"primitive,string" example:"2019-10-12T07:20:50.52Z"` + ExpirationTimestamp int64 `json:"expirationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648164657"` // PaymentURL secure HTTPS location where a Payment message (see below) will be sent to obtain a PaymentACK. // Maximum length is 4000 characters - PaymentURL string `json:"paymentUrl" example:"https://localhost:3443/api/v1/payment/123456"` - // Memo Optional note that should be displayed to the customer, explaining what this PaymentRequest is for. + PaymentURL string `json:"paymentUrl" binding:"required" example:"http://localhost:3443/api/v1/payment/123456"` + // Memo note that should be displayed to the customer, explaining what this PaymentRequest is for. // Maximum length is 50 characters. - Memo string `json:"memo" example:"invoice number 123456"` - // MerchantData contains arbitrary data that may be used by the payment host to identify the PaymentRequest. + // Optional. + Memo string `json:"memo,omitempty" example:"invoice number 123456"` + // Beneficiary Arbitrary data that may be used by the payment host to identify the PaymentRequest // May be omitted if the payment host does not need to associate Payments with PaymentRequest // or if they associate each PaymentRequest with a separate payment address. // Maximum length is 10000 characters. - MerchantData *Merchant `json:"merchantData,omitempty"` - // FeeRate defines the amount of fees a users wallet should add to the payment - // when submitting their final payments. - FeeRate *bt.FeeQuote `json:"fees"` + // Optional. + Beneficiary *Merchant `json:"beneficiary,omitempty"` + // Modes TSC payment modes specified by ID (and well defined) modes customer can choose to pay + // A key-value map. required field but not if legacy BIP270 outputs are provided + Modes *PaymentModes `json:"modes"` } // PaymentRequestArgs are request arguments that can be passed to the service. From 0d2956038099439542cecdbf4a9bf59ea483b71c Mon Sep 17 00:00:00 2001 From: AustEcon Date: Thu, 28 Jul 2022 21:34:35 +1200 Subject: [PATCH 2/8] Fix linting errors --- destinations.go | 6 ++++-- payment_request.go | 44 +++++++++++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/destinations.go b/destinations.go index a9ff527..9cf0dcb 100644 --- a/destinations.go +++ b/destinations.go @@ -4,11 +4,13 @@ import ( "github.com/libsv/go-bt/v2/bscript" ) -type DPPNativeOutput struct { + +// NativeOutput defines a native type output as opposed to a token for example. +type NativeOutput struct { // Amount is the number of satoshis to be paid. Amount uint64 `json:"amount" binding:"required" example:"100000"` // Script is a locking script where payment should be sent, formatted as a hexadecimal string. LockingScript *bscript.Script `json:"script" binding:"required" swaggertype:"primitive,string" example:"76a91455b61be43392125d127f1780fb038437cd67ef9c88ac"` // Description, an optional description such as "tip" or "sales tax". Maximum length is 100 chars. Description string `json:"description,omitempty" example:"paymentReference 123456"` -} \ No newline at end of file +} diff --git a/payment_request.go b/payment_request.go index f252ff4..25a6584 100644 --- a/payment_request.go +++ b/payment_request.go @@ -8,33 +8,43 @@ import ( // See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol -type DPPPolicies struct { +// Policies An object containing some policy information like fees or whether Ancestors are +// required in the `Payment`. +type Policies struct { // FeeRate defines the amount of fees a users wallet should add to the payment // when submitting their final payments. - FeeRate map[string]map[string]int `json:"fees,omitempty"` - SPVRequired bool `json:"SPVRequired,omitempty"` - LockTime uint32 `json:"lockTime,omitempty"` + FeeRate map[string]map[string]int `json:"fees,omitempty"` + SPVRequired bool `json:"SPVRequired,omitempty"` + LockTime uint32 `json:"lockTime,omitempty"` } -type DPPInput struct { - ScriptSig string `json:"scriptSig" binding:"required"`// string. required. - TxId string `json:"txid" binding:"required"`// string. required. - Vout uint32 `json:"vout" binding:"required"`// integer. required. - Value uint64 `json:"value" binding:"required"`// integer. required. - NSequence int `json:"nSequence,omitempty"` // number. optional. +// NativeInput a way of declaring requirements for the inputs which should be used. +type NativeInput struct { + ScriptSig string `json:"scriptSig" binding:"required"` // string. required. + TxID string `json:"txid" binding:"required"` // string. required. + Vout uint32 `json:"vout" binding:"required"` // integer. required. + Value uint64 `json:"value" binding:"required"` // integer. required. + NSequence int `json:"nSequence,omitempty"` // number. optional. } +// Inputs provides options of different arrays of input script types. +// Currently, only "native" type input are supported. +type Inputs struct { + NativeOutputs []NativeInput `json:"native"` +} -type DPPOutputs struct { - NativeOutputs []DPPNativeOutput `json:"native"` +// Outputs provides options of different arrays of output script types. +// Currently, only "native" type outputs are supported. +type Outputs struct { + NativeOutputs []NativeOutput `json:"native"` } // TransactionTerms a single definition of requested transaction format for the standard payment mode: // "ef63d9775da5" in the DPP TSC spec. type TransactionTerms struct { - Outputs DPPOutputs `json:"outputs"` - Inputs []DPPInput `json:"inputs,omitempty"` - Policies *DPPPolicies `json:"policies"` + Outputs Outputs `json:"outputs"` + Inputs Inputs `json:"inputs,omitempty"` + Policies *Policies `json:"policies"` } // PaymentModes message used in DPP TSC spec. @@ -56,7 +66,7 @@ type PaymentRequest struct { Version string `json:"version" binding:"required" example:"1.0"` // Outputs an array of outputs. DEPRECATED but included for backward compatibility. // Optional. - Outputs []DPPNativeOutput `json:"outputs,omitempty"` + Outputs []NativeOutput `json:"outputs,omitempty"` // CreationTimestamp Unix timestamp (seconds since 1-Jan-1970 UTC) when the PaymentRequest was created. // Required. CreationTimestamp int64 `json:"creationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648163657"` @@ -78,7 +88,7 @@ type PaymentRequest struct { Beneficiary *Merchant `json:"beneficiary,omitempty"` // Modes TSC payment modes specified by ID (and well defined) modes customer can choose to pay // A key-value map. required field but not if legacy BIP270 outputs are provided - Modes *PaymentModes `json:"modes"` + Modes *PaymentModes `json:"modes"` } // PaymentRequestArgs are request arguments that can be passed to the service. From eb0655399a4a75166df2b4cb2b11814197d5a832 Mon Sep 17 00:00:00 2001 From: AustEcon Date: Sat, 30 Jul 2022 00:59:37 +1200 Subject: [PATCH 3/8] Updates to `Payment` and `PaymentACK` message structures for TSC compliance - Rename `PaymentRequest` -> `PaymentTerms` as `PaymentRequest` was the old BIP270 terminology for this struct - Update the payment.go:Validate() method for the new `Payment` struct --- destinations.go | 1 - merchant.go | 4 +- mocks/payment_request_service.go | 4 +- payment.go | 138 +++----- payment_ack.go | 62 ++++ payment_request.go | 11 +- payment_test.go | 77 ++-- .../libsv/go-bc/spv/ancestry_binary.go | 142 ++++++++ .../libsv/go-bc/spv/ancestry_json.go | 100 ++++++ .../libsv/go-bc/spv/ancestry_tsc_json.go | 112 ++++++ .../libsv/go-bc/spv/create_ancestry.go | 66 ++++ vendor/github.com/libsv/go-bc/spv/creator.go | 47 +++ vendor/github.com/libsv/go-bc/spv/errors.go | 70 ++++ vendor/github.com/libsv/go-bc/spv/verifier.go | 156 +++++++++ .../libsv/go-bc/spv/verifymerkleproof.go | 328 ++++++++++++++++++ .../libsv/go-bc/spv/verifypayment.go | 116 +++++++ vendor/modules.txt | 1 + 17 files changed, 1289 insertions(+), 146 deletions(-) create mode 100644 payment_ack.go create mode 100644 vendor/github.com/libsv/go-bc/spv/ancestry_binary.go create mode 100644 vendor/github.com/libsv/go-bc/spv/ancestry_json.go create mode 100644 vendor/github.com/libsv/go-bc/spv/ancestry_tsc_json.go create mode 100644 vendor/github.com/libsv/go-bc/spv/create_ancestry.go create mode 100644 vendor/github.com/libsv/go-bc/spv/creator.go create mode 100644 vendor/github.com/libsv/go-bc/spv/errors.go create mode 100644 vendor/github.com/libsv/go-bc/spv/verifier.go create mode 100644 vendor/github.com/libsv/go-bc/spv/verifymerkleproof.go create mode 100644 vendor/github.com/libsv/go-bc/spv/verifypayment.go diff --git a/destinations.go b/destinations.go index 9cf0dcb..56ede9a 100644 --- a/destinations.go +++ b/destinations.go @@ -4,7 +4,6 @@ import ( "github.com/libsv/go-bt/v2/bscript" ) - // NativeOutput defines a native type output as opposed to a token for example. type NativeOutput struct { // Amount is the number of satoshis to be paid. diff --git a/merchant.go b/merchant.go index 91c0413..bcfa9ce 100644 --- a/merchant.go +++ b/merchant.go @@ -1,7 +1,7 @@ package dpp -// Merchant to be displayed to the user. -type Merchant struct { +// Beneficiary to be displayed to the user. +type Beneficiary struct { // AvatarURL displays a canonical url to a merchants avatar. AvatarURL string `json:"avatar" example:"http://url.com"` // Name is a human readable string identifying the merchant. diff --git a/mocks/payment_request_service.go b/mocks/payment_request_service.go index a3b249a..54d8dbd 100644 --- a/mocks/payment_request_service.go +++ b/mocks/payment_request_service.go @@ -30,7 +30,7 @@ var _ dpp.PaymentRequestService = &PaymentRequestServiceMock{} // } type PaymentRequestServiceMock struct { // PaymentRequestFunc mocks the PaymentRequest method. - PaymentRequestFunc func(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentRequest, error) + PaymentRequestFunc func(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentTerms, error) // calls tracks calls to the methods. calls struct { @@ -46,7 +46,7 @@ type PaymentRequestServiceMock struct { } // PaymentRequest calls PaymentRequestFunc. -func (mock *PaymentRequestServiceMock) PaymentRequest(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentRequest, error) { +func (mock *PaymentRequestServiceMock) PaymentRequest(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentTerms, error) { if mock.PaymentRequestFunc == nil { panic("PaymentRequestServiceMock.PaymentRequestFunc: method is nil but PaymentRequestService.PaymentRequest was just called") } diff --git a/payment.go b/payment.go index 32c0a41..17a71c6 100644 --- a/payment.go +++ b/payment.go @@ -1,66 +1,62 @@ package dpp import ( - "context" - - "github.com/libsv/go-bt/v2" - "github.com/pkg/errors" + "github.com/libsv/go-bc/spv" validator "github.com/theflyingcodr/govalidator" ) +// These structures are defined in the TSC spec: +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +// HybridPaymentModePayment includes data required for hybrid payment mode. +type HybridPaymentModePayment struct { + // OptionID ID of chosen payment options + OptionID string `json:"optionId"` + // Transactions A list of valid, signed Bitcoin transactions that fully pays the PaymentTerms. + // The transaction is hex-encoded and must NOT be prefixed with “0x”. + // The order of transactions should match the order from PaymentTerms for this mode. + Transactions []string `json:"transactions"` + // Ancestors a map of txid to ancestry transaction info for the transactions in above + // each ancestor contains the TX together with the MerkleProof needed when SPVRequired is true. + // See: https://tsc.bitcoinassociation.net/standards/transaction-ancestors/ + Ancestors map[string]spv.TSCAncestryJSON `json:"ancestors"` +} + +// Originator Data about payer. This data might be needed in many cases, e.g. tracking data for later loyalty +// points processing etc. +type Originator struct { + // Name name of payer. + Name string `json:"name"` + // Paymail Payer’s paymail (where e.g. refunds will be send, identity can be use somehow etc.). + Paymail string `json:"paymail"` + // Avatar URL to an avatar. + Avatar string `json:"avatar"` + // ExtendedData additional optional data. + ExtendedData map[string]interface{} `json:"extendedData"` +} + // Payment is a Payment message used in BIP270. // See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#payment type Payment struct { - // MerchantData is copied from PaymentDetails.merchantData. - // Payment hosts may use invoice numbers or any other data they require to match Payments to PaymentRequests. - // Note that malicious clients may modify the merchantData, so should be authenticated - // in some way (for example, signed with a payment host-only key). - // Maximum length is 10000 characters. - MerchantData Merchant `json:"merchantData"` - // RefundTo is a paymail to send a refund to should a refund be necessary. - // Maximum length is 100 characters - RefundTo *string `json:"refundTo" swaggertype:"primitive,string" example:"me@paymail.com"` - // Memo is a plain-text note from the customer to the payment host. - Memo string `json:"memo" example:"for invoice 123456"` - // Ancestry which contains the details of previous transaction and Merkle proof of each input UTXO. - // Should be available if AncestryRequired is set to true in the paymentRequest. - // See https://tsc.bitcoinassociation.net/standards/spv-envelope/ - Ancestry *string `json:"ancestry"` - // RawTX should be sent if AncestryRequired is set to false in the payment request. - RawTx *string `json:"rawTx"` - // ProofCallbacks are optional and can be supplied when the sender wants to receive - // a merkleproof for the transaction they are submitting as part of the SPV Envelope. - // - // This is especially useful if they are receiving change and means when they use it - // as an input, they can provide the merkle proof. - ProofCallbacks map[string]ProofCallback `json:"proofCallbacks"` + // ModeID chosen from possible modes of PaymentTerms. + ModeID string `json:"modeId" binding:"required" example:"ef63d9775da5"` + // Mode Object with data required by specific mode, e.g. HybridPaymentMode + Mode HybridPaymentModePayment `json:"mode" binding:"required"` + // Originator Data about payer. This data might be needed in many cases, e.g. refund, tract data for later loyalty points processing etc. + Originator Originator `json:"originator"` + // Transaction A single valid, signed Bitcoin transaction that fully pays the PaymentTerms. This field is deprecated. + Transaction *string `json:"transaction,omitempty"` + // Memo A plain-text note from the customer to the payment host. + Memo string `json:"memo,omitempty"` } // Validate will ensure the users request is correct. func (p Payment) Validate() error { v := validator.New(). - Validate("ancestry/rawTx", func() error { - if p.RawTx == nil { - return errors.New("either ancestry or a rawTX are required") - } - return nil - }). - Validate("merchantData.extendedData", validator.NotEmpty(p.MerchantData.ExtendedData)) - if p.MerchantData.ExtendedData != nil { - v = v.Validate("merchantData.paymentReference", validator.NotEmpty(p.MerchantData.ExtendedData["paymentReference"])) - } - - if p.RawTx != nil { - v = v.Validate("rawTx", func() error { - if _, err := bt.NewTxFromString(*p.RawTx); err != nil { - return errors.Wrap(err, "invalid rawTx supplied") - } - return nil - }) - } - if p.RefundTo != nil { - v = v.Validate("refundTo", validator.StrLength(*p.RefundTo, 0, 100)) - } + Validate("modeId", validator.NotEmpty(p.ModeID)). + Validate("mode", validator.NotEmpty(p.Mode)). + Validate("mode.optionId", validator.NotEmpty(p.Mode.OptionID)). + Validate("mode.transactions", validator.NotEmpty(p.Mode.Transactions)) return v.Err() } @@ -69,47 +65,3 @@ func (p Payment) Validate() error { type ProofCallback struct { Token string `json:"token"` } - -// PaymentACK message used in BIP270. -// See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#paymentack -type PaymentACK struct { - ID string `json:"id"` - TxID string `json:"tx_id"` - Memo string `json:"memo"` - PeerChannel *PeerChannelData `json:"peer_channel"` - // A number indicating why the transaction was not accepted. 0 or undefined indicates no error. - // A 1 or any other positive integer indicates an error. The errors are left undefined for now; - // it is recommended only to use “1” and to fill the memo with a textual explanation about why - // the transaction was not accepted until further numbers are defined and standardised. - Error int `json:"error,omitempty"` -} - -// PeerChannelData holds peer channel information for subscribing to and reading from a peer channel. -type PeerChannelData struct { - Host string `json:"host"` - Path string `json:"path"` - ChannelID string `json:"channel_id"` - Token string `json:"token"` -} - -// PaymentCreateArgs identifies the paymentID used for the payment. -type PaymentCreateArgs struct { - PaymentID string `param:"paymentID"` -} - -// Validate will ensure that the PaymentCreateArgs are supplied and correct. -func (p PaymentCreateArgs) Validate() error { - return validator.New(). - Validate("paymentID", validator.NotEmpty(p.PaymentID)). - Err() -} - -// PaymentService enforces business rules when creating payments. -type PaymentService interface { - PaymentCreate(ctx context.Context, args PaymentCreateArgs, req Payment) (*PaymentACK, error) -} - -// PaymentWriter will write a payment to a data store. -type PaymentWriter interface { - PaymentCreate(ctx context.Context, args PaymentCreateArgs, req Payment) (*PaymentACK, error) -} diff --git a/payment_ack.go b/payment_ack.go new file mode 100644 index 0000000..038dbf8 --- /dev/null +++ b/payment_ack.go @@ -0,0 +1,62 @@ +package dpp + +import ( + "context" + validator "github.com/theflyingcodr/govalidator" +) + +// HybridPaymentModeACK includes data required for hybrid payment mode. +type HybridPaymentModeACK struct { + TransactionIds []string `json:"transactionIds"` + PeerChannel *PeerChannelData `json:"peerChannel"` +} + +// PaymentACK message used in the TSC DPP spec. +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol/#PaymentModes +type PaymentACK struct { + // ModeID the chosen mode. + ModeID string `json:"modeId" binding:"required" example:"ef63d9775da5"` + // Mode data required by specific payment mode + Mode *HybridPaymentModeACK `json:"mode"` + PeerChannel *PeerChannelData `json:"peerChannel"` + RedirectURL string `json:"redirectUrl"` + + // Memo may contain information about why there was an error. This field is poorly defined until + // error reporting is more standardised. + Memo string + // A number indicating why the transaction was not accepted. 0 or undefined indicates no error. + // A 1 or any other positive integer indicates an error. The errors are left undefined for now; + // it is recommended only to use “1” and to fill the memo with a textual explanation about why + // the transaction was not accepted until further numbers are defined and standardised. + Error int `json:"error,omitempty"` +} + +// PeerChannelData holds peer channel information for subscribing to and reading from a peer channel. +type PeerChannelData struct { + Host string `json:"host"` + Path string `json:"path"` + ChannelID string `json:"channel_id"` + Token string `json:"token"` +} + +// PaymentCreateArgs identifies the paymentID used for the payment. +type PaymentCreateArgs struct { + PaymentID string `param:"paymentID"` +} + +// Validate will ensure that the PaymentCreateArgs are supplied and correct. +func (p PaymentCreateArgs) Validate() error { + return validator.New(). + Validate("paymentID", validator.NotEmpty(p.PaymentID)). + Err() +} + +// PaymentService enforces business rules when creating payments. +type PaymentService interface { + PaymentCreate(ctx context.Context, args PaymentCreateArgs, req Payment) (*PaymentACK, error) +} + +// PaymentWriter will write a payment to a data store. +type PaymentWriter interface { + PaymentCreate(ctx context.Context, args PaymentCreateArgs, req Payment) (*PaymentACK, error) +} diff --git a/payment_request.go b/payment_request.go index 25a6584..614796a 100644 --- a/payment_request.go +++ b/payment_request.go @@ -7,7 +7,6 @@ import ( // These structures are defined in the TSC spec: // See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol - // Policies An object containing some policy information like fees or whether Ancestors are // required in the `Payment`. type Policies struct { @@ -43,7 +42,7 @@ type Outputs struct { // "ef63d9775da5" in the DPP TSC spec. type TransactionTerms struct { Outputs Outputs `json:"outputs"` - Inputs Inputs `json:"inputs,omitempty"` + Inputs Inputs `json:"inputs,omitempty"` Policies *Policies `json:"policies"` } @@ -54,8 +53,8 @@ type PaymentModes struct { HybridPaymentMode map[string]map[string][]TransactionTerms `json:"ef63d9775da5"` } -// PaymentRequest message as defined in the DPP T$C spec. -type PaymentRequest struct { +// PaymentTerms message as defined in the DPP T$C spec. +type PaymentTerms struct { // Network Always set to "bitcoin" (but seems to be set to 'bitcoin-sv' // outside bip270 spec, see https://handcash.github.io/handcash-merchant-integration/#/merchant-payments) // {enum: bitcoin, bitcoin-sv, test} @@ -85,7 +84,7 @@ type PaymentRequest struct { // or if they associate each PaymentRequest with a separate payment address. // Maximum length is 10000 characters. // Optional. - Beneficiary *Merchant `json:"beneficiary,omitempty"` + Beneficiary *Beneficiary `json:"beneficiary,omitempty"` // Modes TSC payment modes specified by ID (and well defined) modes customer can choose to pay // A key-value map. required field but not if legacy BIP270 outputs are provided Modes *PaymentModes `json:"modes"` @@ -105,5 +104,5 @@ type PaymentRequestService interface { // PaymentRequestReader will return a new payment request. type PaymentRequestReader interface { - PaymentRequest(ctx context.Context, args PaymentRequestArgs) (*PaymentRequest, error) + PaymentRequest(ctx context.Context, args PaymentRequestArgs) (*PaymentTerms, error) } diff --git a/payment_test.go b/payment_test.go index 5ffd815..77456ce 100644 --- a/payment_test.go +++ b/payment_test.go @@ -14,64 +14,57 @@ func TestPaymentCreate_Validate(t *testing.T) { }{ "valid request should return no errors": { req: Payment{ - MerchantData: Merchant{ + ModeID: "ef63d9775da5", + Mode: HybridPaymentModePayment{ + OptionID: "choiceID0", + Transactions: []string{"tx1", "tx2"}, + }, + Originator: Originator{ + Name: "Bob The Builder", ExtendedData: map[string]interface{}{ "paymentReference": "abc123", }, }, - ProofCallbacks: map[string]ProofCallback{ - "http://myproofs/proofs": { - Token: "abc123", - }, - }, - RawTx: func() *string { - s := "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000" - return &s - }(), - Memo: "test this please", - }, - }, "merchant data missing extended data should error": { - req: Payment{ - RawTx: func() *string { + Transaction: func() *string { s := "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000" return &s }(), - Memo: "test this please", }, - exp: "[merchantData.extendedData: value cannot be empty]", - }, "merchant data missing payment reference should error": { + }, "mode id missing should error": { req: Payment{ - RawTx: func() *string { - s := "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000" - return &s - }(), - MerchantData: Merchant{ExtendedData: map[string]interface{}{"test": "value"}}, - Memo: "test this please", + Mode: HybridPaymentModePayment{ + OptionID: "choiceID0", + Transactions: []string{ + "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000", + }, + }, + Originator: Originator{ + Name: "Bob The Builder", + ExtendedData: map[string]interface{}{ + "paymentReference": "abc123", + }, + }, }, - exp: "[merchantData.paymentReference: value cannot be empty]", - }, "refundTo too long should error": { + exp: "[modeId: value cannot be empty]", + }, "mode data missing payment should error": { req: Payment{ - RawTx: func() *string { - s := "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000" - return &s - }(), - RefundTo: func() *string { - bb := make([]byte, 0) - // generate string 1 more byte than 10000 - for i := 0; i <= 100; i++ { - bb = append(bb, 42) - } - out := string(bb) - return &out - }(), - MerchantData: Merchant{ + ModeID: "ef63d9775da5", + Originator: Originator{ + Name: "Bob The Builder", ExtendedData: map[string]interface{}{ "paymentReference": "abc123", }, }, - Memo: "test this please", }, - exp: "[refundTo: value must be between 0 and 100 characters]", + exp: "[mode.optionId: value cannot be empty], [mode.transactions: value cannot be empty], [mode: value cannot be empty]", + }, "mode missing transaction field should error": { + req: Payment{ + ModeID: "ef63d9775da5", + Mode: HybridPaymentModePayment{ + OptionID: "choiceID0", + }, + }, + exp: "[mode.transactions: value cannot be empty]", }, } for name, test := range tests { diff --git a/vendor/github.com/libsv/go-bc/spv/ancestry_binary.go b/vendor/github.com/libsv/go-bc/spv/ancestry_binary.go new file mode 100644 index 0000000..73f460f --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/ancestry_binary.go @@ -0,0 +1,142 @@ +package spv + +import ( + "github.com/libsv/go-bk/crypto" + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript" + + "github.com/libsv/go-bc" +) + +const ( + flagTx = byte(1) + flagProof = byte(2) + flagMapi = byte(3) +) + +// Payment is a payment and its ancestry. +type Payment struct { + PaymentTx *bt.Tx + Ancestry []byte +} + +// binaryChunk is a clear way to pass around chunks while keeping their type explicit. +type binaryChunk struct { + ContentType byte + Data []byte +} + +type extendedInput struct { + input *bt.Input + vin int +} + +type ancestry struct { + Tx *bt.Tx + Proof []byte + MapiResponses []*bc.MapiCallback +} + +// parseAncestry creates a new struct from the bytes of a txContext. +func parseAncestry(b []byte) (map[[32]byte]*ancestry, error) { + + if b[0] != 1 { // the first byte is the version number. + return nil, ErrUnsupporredVersion + } + offset := uint64(1) + total := uint64(len(b)) + aa := make(map[[32]byte]*ancestry) + + var TxID [32]byte + + if total == offset { + return nil, ErrCannotCalculateFeePaid + } + + // first Data must be a Tx + if b[offset] != 1 { + return nil, ErrTipTxConfirmed + } + + for total > offset { + chunk, size := parseChunk(b, offset) + offset += size + switch chunk.ContentType { + case flagTx: + hash := crypto.Sha256d(chunk.Data) + copy(TxID[:], bt.ReverseBytes(hash)) // fixed size array from slice. + tx, err := bt.NewTxFromBytes(chunk.Data) + if err != nil { + return nil, err + } + if len(tx.Inputs) == 0 { + return nil, ErrNoTxInputsToVerify + } + aa[TxID] = &ancestry{ + Tx: tx, + } + case flagProof: + aa[TxID].Proof = chunk.Data + case flagMapi: + callBacks, err := parseMapiCallbacks(chunk.Data) + if err != nil { + return nil, err + } + aa[TxID].MapiResponses = callBacks + default: + continue + } + } + return aa, nil +} + +func parseChunk(b []byte, start uint64) (binaryChunk, uint64) { + offset := start + typeOfNextData := b[offset] + offset++ + l, size := bt.NewVarIntFromBytes(b[offset:]) + offset += uint64(size) + chunk := binaryChunk{ + ContentType: typeOfNextData, + Data: b[offset : offset+uint64(l)], + } + offset += uint64(l) + return chunk, offset - start +} + +func parseMapiCallbacks(b []byte) ([]*bc.MapiCallback, error) { + if len(b) == 0 { + return nil, ErrTriedToParseZeroBytes + } + var internalOffset uint64 + allBinary := uint64(len(b)) + numOfMapiResponses := b[internalOffset] + if numOfMapiResponses == 0 && len(b) == 1 { + return nil, ErrTriedToParseZeroBytes + } + internalOffset++ + + var responses = [][]byte{} + for allBinary > internalOffset { + l, size := bt.NewVarIntFromBytes(b[internalOffset:]) + internalOffset += uint64(size) + response := b[internalOffset : internalOffset+uint64(l)] + internalOffset += uint64(l) + responses = append(responses, response) + } + + mapiResponses := make([]*bc.MapiCallback, 0) + for _, response := range responses { + mapiResponse, err := bc.NewMapiCallbackFromBytes(response) + if err != nil { + return nil, err + } + mapiResponses = append(mapiResponses, mapiResponse) + } + return mapiResponses, nil +} + +func verifyInputOutputPair(tx *bt.Tx, lock *bscript.Script, unlock *bscript.Script) bool { + // TODO script interpreter. + return true +} diff --git a/vendor/github.com/libsv/go-bc/spv/ancestry_json.go b/vendor/github.com/libsv/go-bc/spv/ancestry_json.go new file mode 100644 index 0000000..272b446 --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/ancestry_json.go @@ -0,0 +1,100 @@ +package spv + +import ( + "encoding/hex" + + "github.com/libsv/go-bt/v2" + "github.com/pkg/errors" + + "github.com/libsv/go-bc" +) + +// AncestryJSON is a struct which contains all information needed for a transaction to be verified. +// this contains all ancestors for the transaction allowing proofs etc to be verified. +// +// NOTE: this is the JSON format of the Ancestry but in a nested format (in comparison) with +// the flat structure that the TSC uses. This allows verification to become a lot easier and +// use a recursive function. +type AncestryJSON struct { + TxID string `json:"txid,omitempty"` + RawTx string `json:"rawTx,omitempty"` + Proof *bc.MerkleProof `json:"proof,omitempty"` + MapiResponses []bc.MapiCallback `json:"mapiResponses,omitempty"` + Parents map[string]*AncestryJSON `json:"parents,omitempty"` +} + +// IsAnchored returns true if the ancestry has a merkle proof. +func (e *AncestryJSON) IsAnchored() bool { + return e.Proof != nil +} + +// HasParents returns true if this ancestry has immediate parents. +func (e *AncestryJSON) HasParents() bool { + return e.Parents != nil && len(e.Parents) > 0 +} + +// ParentTx will return a parent if found and convert the rawTx to a bt.TX, otherwise a ErrNotAllInputsSupplied error is returned. +func (e *AncestryJSON) ParentTx(txID string) (*bt.Tx, error) { + env, ok := e.Parents[txID] + if !ok { + return nil, errors.Wrapf(ErrNotAllInputsSupplied, "expected parent tx %s is missing", txID) + } + return bt.NewTxFromString(env.RawTx) +} + +// Bytes takes a TxAncestry struct and returns the serialised binary format. +func (e *AncestryJSON) Bytes() ([]byte, error) { + ancestryBinary := make([]byte, 0) + ancestryBinary = append(ancestryBinary, 1) // Binary format version 1 + binary, err := serialiseInputs(e.Parents) + if err != nil { + return nil, err + } + ancestryBinary = append(ancestryBinary, binary...) + return ancestryBinary, nil +} + +func serialiseInputs(parents map[string]*AncestryJSON) ([]byte, error) { + binary := make([]byte, 0) + for _, input := range parents { + currentTx, err := hex.DecodeString(input.RawTx) + if err != nil { + return nil, err + } + dataLength := bt.VarInt(uint64(len(currentTx))) + binary = append(binary, flagTx) // first data will always be a rawTx. + binary = append(binary, dataLength.Bytes()...) // of this length. + binary = append(binary, currentTx...) // the data. + if input.MapiResponses != nil && len(input.MapiResponses) > 0 { + binary = append(binary, flagMapi) // next data will be a mapi response. + numMapis := bt.VarInt(uint64(len(input.MapiResponses))) + binary = append(binary, numMapis.Bytes()...) // number of mapi reponses which follow + for _, mapiResponse := range input.MapiResponses { + mapiR, err := mapiResponse.Bytes() + if err != nil { + return nil, err + } + dataLength := bt.VarInt(uint64(len(mapiR))) + binary = append(binary, dataLength.Bytes()...) // of this length. + binary = append(binary, mapiR...) // the data. + } + } + if input.Proof != nil { + proof, err := input.Proof.Bytes() + if err != nil { + return nil, errors.Wrap(err, "Failed to serialise this input's proof struct") + } + proofLength := bt.VarInt(uint64(len(proof))) + binary = append(binary, flagProof) // it's going to be a proof. + binary = append(binary, proofLength.Bytes()...) // of this length. + binary = append(binary, proof...) // the data. + } else if input.HasParents() { + parentsBinary, err := serialiseInputs(input.Parents) + if err != nil { + return nil, err + } + binary = append(binary, parentsBinary...) + } + } + return binary, nil +} diff --git a/vendor/github.com/libsv/go-bc/spv/ancestry_tsc_json.go b/vendor/github.com/libsv/go-bc/spv/ancestry_tsc_json.go new file mode 100644 index 0000000..e897923 --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/ancestry_tsc_json.go @@ -0,0 +1,112 @@ +package spv + +import ( + "encoding/hex" + + "github.com/libsv/go-bc" + "github.com/libsv/go-bt/v2" +) + +// TSCAncestriesJSON spec at https://tsc.bitcoinassociation.net/standards/transaction-ancestors/ eventually. +type TSCAncestriesJSON []TSCAncestryJSON + +// TSCAncestryJSON is one of the serial objects within the overall list of ancestors. +// +// NOTE: This JSON structure follows the TSC definition even though the other JSON +// structure used in ancestry_json.go is more useful for verification. +type TSCAncestryJSON struct { + RawTx string `json:"rawtx,omitempty"` + Proof *bc.MerkleProof `json:"proof,omitempty"` + MapiResponses []*bc.MapiCallback `json:"mapiResponses,omitempty"` +} + +// NewAncestryJSONFromBytes is a way to create the JSON format for Ancestry from the binary format. +func NewAncestryJSONFromBytes(b []byte) (TSCAncestriesJSON, error) { + ancestry, err := parseAncestry(b) + if err != nil { + return nil, err + } + ancestors := make([]TSCAncestryJSON, 0) + for _, ancestor := range ancestry { + rawTx := ancestor.Tx.String() + a := TSCAncestryJSON{ + RawTx: rawTx, + MapiResponses: ancestor.MapiResponses, + } + if ancestor.Proof != nil { + mpb, err := parseBinaryMerkleProof(ancestor.Proof) + if err != nil { + return nil, err + } + a.Proof = &bc.MerkleProof{ + Index: mpb.index, + TxOrID: mpb.txOrID, + Target: mpb.target, + Nodes: mpb.nodes, + ProofType: flagProofType(mpb.flags), + } + } + ancestors = append(ancestors, a) + } + return ancestors, nil +} + +// Bytes takes an AncestryJSON and returns the serialised bytes. +func (j TSCAncestriesJSON) Bytes() ([]byte, error) { + binaryTxContext := make([]byte, 0) + + // Binary format version 1. + binaryTxContext = append(binaryTxContext, 1) + + // follow with the list of ancestors, including their proof or mapi responses if present. + for _, ancestor := range j { + rawTx, err := hex.DecodeString(ancestor.RawTx) + if err != nil { + return nil, err + } + length := bt.VarInt(uint64(len(rawTx))) + binaryTxContext = append(binaryTxContext, flagTx) + binaryTxContext = append(binaryTxContext, length.Bytes()...) + binaryTxContext = append(binaryTxContext, rawTx...) + if ancestor.Proof != nil { + rawProof, err := ancestor.Proof.Bytes() + if err != nil { + return nil, err + } + length := bt.VarInt(uint64(len(rawProof))) + binaryTxContext = append(binaryTxContext, flagProof) + binaryTxContext = append(binaryTxContext, length.Bytes()...) + binaryTxContext = append(binaryTxContext, rawProof...) + } + if ancestor.MapiResponses != nil && len(ancestor.MapiResponses) > 0 { + binaryTxContext = append(binaryTxContext, flagMapi) + numOfMapiResponses := bt.VarInt(uint64(len(ancestor.MapiResponses))) + binaryTxContext = append(binaryTxContext, numOfMapiResponses.Bytes()...) + for _, mapiResponse := range ancestor.MapiResponses { + mapiR, err := mapiResponse.Bytes() + if err != nil { + return nil, err + } + dataLength := bt.VarInt(uint64(len(mapiR))) + binaryTxContext = append(binaryTxContext, dataLength.Bytes()...) + binaryTxContext = append(binaryTxContext, mapiR...) + } + } + } + + return binaryTxContext, nil +} + +func flagProofType(flags byte) string { + switch flags & targetTypeFlags { + // if bits 1 and 2 of flags are NOT set, target should contain a block hash (32 bytes). + // if bit 2 of flags is set, target should contain a merkle root (32 bytes). + case 0, 4: + return "blockhash" + // if bit 1 of flags is set, target should contain a block header (80 bytes). + case 2: + return "header" + default: + return "" + } +} diff --git a/vendor/github.com/libsv/go-bc/spv/create_ancestry.go b/vendor/github.com/libsv/go-bc/spv/create_ancestry.go new file mode 100644 index 0000000..35ede7b --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/create_ancestry.go @@ -0,0 +1,66 @@ +package spv + +import ( + "context" + "fmt" + + "github.com/libsv/go-bt/v2" + "github.com/pkg/errors" +) + +// CreateTxAncestry builds and returns an spv.TxAncestry for the provided tx. +func (c *creator) CreateTxAncestry(ctx context.Context, tx *bt.Tx) (*AncestryJSON, error) { + if len(tx.Inputs) == 0 { + return nil, ErrNoTxInputs + } + + ancestry := &AncestryJSON{ + TxID: tx.TxID(), + RawTx: tx.String(), + Parents: make(map[string]*AncestryJSON), + } + + for _, input := range tx.Inputs { + pTxID := input.PreviousTxIDStr() + + // If we already have added the tx to the parent ancestry, there's no point in + // redoing the same work + if _, ok := ancestry.Parents[pTxID]; ok { + continue + } + + // Build a *bt.Tx from its TxID and recursively call this function building + // for inputs without proofs, until a parent with a Merkle Proof is found. + pTx, err := c.txc.Tx(ctx, pTxID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get tx %s", pTxID) + } + if pTx == nil { + return nil, fmt.Errorf("could not find tx %s", pTxID) + } + + // Check the store for a Merkle Proof for the current input. + mp, err := c.mpc.MerkleProof(ctx, pTxID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get merkle proof for tx %s", pTxID) + } + // If a Merkle Proof is found, create the ancestry and skip any further recursion + if mp != nil { + ancestry.Parents[pTxID] = &AncestryJSON{ + RawTx: pTx.String(), + TxID: pTxID, + Proof: mp, + } + continue + } + + pEnvelope, err := c.CreateTxAncestry(ctx, pTx) + if err != nil { + return nil, err + } + + ancestry.Parents[pTxID] = pEnvelope + } + + return ancestry, nil +} diff --git a/vendor/github.com/libsv/go-bc/spv/creator.go b/vendor/github.com/libsv/go-bc/spv/creator.go new file mode 100644 index 0000000..3fc12ae --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/creator.go @@ -0,0 +1,47 @@ +package spv + +import ( + "context" + + "github.com/libsv/go-bt/v2" + "github.com/pkg/errors" + + "github.com/libsv/go-bc" +) + +// An TxAncestryCreator is an interface used to build the spv.TxAncestry data type for +// Simple Payment Verification (SPV). +// +// The implementation of an spv.TxStore and spv.MerkleProofStore which is supplied will depend +// on the client you are using. +type TxAncestryCreator interface { + CreateTxAncestry(context.Context, *bt.Tx) (*AncestryJSON, error) +} + +// TxStore interfaces the a tx store. +type TxStore interface { + Tx(ctx context.Context, txID string) (*bt.Tx, error) +} + +// MerkleProofStore interfaces a Merkle Proof store. +type MerkleProofStore interface { + MerkleProof(ctx context.Context, txID string) (*bc.MerkleProof, error) +} + +type creator struct { + txc TxStore + mpc MerkleProofStore +} + +// NewEnvelopeCreator creates a new spv.Creator with the provided spv.TxStore and tx.MerkleProofStore. +// If either implementation is not provided, the setup will return an error. +func NewEnvelopeCreator(txc TxStore, mpc MerkleProofStore) (TxAncestryCreator, error) { + if txc == nil { + return nil, errors.New("an spv.TxStore implementation is required") + } + if mpc == nil { + return nil, errors.New("an spv.MerkleProofStore implementation is required") + } + + return &creator{txc: txc, mpc: mpc}, nil +} diff --git a/vendor/github.com/libsv/go-bc/spv/errors.go b/vendor/github.com/libsv/go-bc/spv/errors.go new file mode 100644 index 0000000..482fda3 --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/errors.go @@ -0,0 +1,70 @@ +package spv + +import "github.com/pkg/errors" + +var ( + // ErrNoTxInputs returns if an ancestry is attempted to be created from a transaction that has no inputs. + ErrNoTxInputs = errors.New("provided tx has no inputs to build ancestry from") + + // ErrPaymentNotVerified returns if a transaction in the tree provided was missed during verification. + ErrPaymentNotVerified = errors.New("a tx was missed during validation") + + // ErrTipTxConfirmed returns if the tip transaction is already confirmed. + ErrTipTxConfirmed = errors.New("tip transaction must be unconfirmed") + + // ErrNoConfirmedTransaction returns if a path from tip to beginning/anchor contains no confirmed transaction. + ErrNoConfirmedTransaction = errors.New("not confirmed/anchored tx(s) provided") + + // ErrTxIDMismatch returns if they key value pair of a transactions input has a mismatch in txID. + ErrTxIDMismatch = errors.New("input and proof ID mismatch") + + // ErrNotAllInputsSupplied returns if an unconfirmed transaction in ancestry contains inputs which are not + // present in the parent ancestor. + ErrNotAllInputsSupplied = errors.New("a tx input missing in parent ancestor") + + // ErrNoTxInputsToVerify returns if a transaction has no inputs. + ErrNoTxInputsToVerify = errors.New("a tx has no inputs to verify") + + // ErrNilInitialPayment returns if a transaction has no inputs. + ErrNilInitialPayment = errors.New("initial payment cannot be nil") + + // ErrInputRefsOutOfBoundsOutput returns if a transaction has no inputs. + ErrInputRefsOutOfBoundsOutput = errors.New("tx input index into output is out of bounds") + + // ErrNoFeeQuoteSupplied is returned when VerifyFees is enabled but no bt.FeeQuote has been supplied. + ErrNoFeeQuoteSupplied = errors.New("no bt.FeeQuote supplied for fee validation, supply the bt.FeeQuote using VerifyFees opt") + + // ErrFeePaidNotEnough returned when not enough fees have been paid. + ErrFeePaidNotEnough = errors.New("not enough fees paid") + + // ErrCannotCalculateFeePaid returned when fee check is enabled but the tx has no parents. + ErrCannotCalculateFeePaid = errors.New("no parents supplied in ancestry which means we cannot valdiate " + + "fees, either ensure parents are supplied or remove fee check") + + // ErrInvalidProof is returned if the merkle proof validation fails. + ErrInvalidProof = errors.New("invalid merkle proof, payment invalid") + + // ErrMissingOutput is returned when checking fees if an output in a parent tx is missing. + ErrMissingOutput = errors.New("expected output used in payment tx missing") + + // ErrProofOrInputMissing returns if a path from tip to beginning/anchor is broken. + ErrProofOrInputMissing = errors.New("break in the ancestry missing either a parent transaction or a proof") + + // ErrTriedToParseZeroBytes returns when we attempt to parse a slice of bytes of zero length which should be a mapi response. + ErrTriedToParseZeroBytes = errors.New("there are no mapi response bytes to parse") + + // ErrUnsupporredVersion returns if another version of the binary format is being used - since we cannot guarantee we know how to parse it. + ErrUnsupporredVersion = errors.New("we only support version 1 of the Ancestor Binary format") + + // ErrInvalidMerkleFlags returns if a merkle proof being verified uses something other than the one currently supported. + ErrInvalidMerkleFlags = errors.New("invalid flags used in merkle proof") + + // ErrMissingTxidInProof returns if there's a missing txid in the proof. + ErrMissingTxidInProof = errors.New("missing txid in proof") + + // ErrMissingRootInProof returns if there's a missing root in the proof. + ErrMissingRootInProof = errors.New("missing root in proof") + + // ErrInvalidNodes returns if there is a * on the left hand side within the node array. + ErrInvalidNodes = errors.New("invalid nodes") +) diff --git a/vendor/github.com/libsv/go-bc/spv/verifier.go b/vendor/github.com/libsv/go-bc/spv/verifier.go new file mode 100644 index 0000000..d3066ca --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/verifier.go @@ -0,0 +1,156 @@ +package spv + +import ( + "context" + + "github.com/libsv/go-bt/v2" + "github.com/pkg/errors" + + "github.com/libsv/go-bc" +) + +type verifyOptions struct { + // proofs validation + proofs bool + script bool + fees bool + feeQuote *bt.FeeQuote +} + +// clone will copy the verifyOptions to a new struct and return it. +func (v *verifyOptions) clone() *verifyOptions { + return &verifyOptions{ + proofs: v.proofs, + fees: v.fees, + script: v.script, + feeQuote: v.feeQuote, + } +} + +// VerifyOpt defines a functional option that is used to modify behaviour of +// the payment verifier. +type VerifyOpt func(opts *verifyOptions) + +// VerifyProofs will make the verifier validate the ancestry merkle proofs for each parent transaction. +func VerifyProofs() VerifyOpt { + return func(opts *verifyOptions) { + opts.proofs = true + } +} + +// NoVerifyProofs will switch off ancestry proof verification +// and rely on mAPI/node verification when the tx is broadcast. +func NoVerifyProofs() VerifyOpt { + return func(opts *verifyOptions) { + opts.proofs = false + } +} + +// VerifyFees will make the verifier check the transaction fees +// of the supplied transaction are enough based on the feeQuote +// provided. +// +// It is recommended to provide a fresh fee quote when calling the VerifyPayment +// method rather than loading fees when calling NewPaymentVerifier as fees can go out of date +// over the lifetime of the application and you may be supplying different feeQuotes +// to different consumers. +func VerifyFees(fees *bt.FeeQuote) VerifyOpt { + return func(opts *verifyOptions) { + opts.fees = true + opts.feeQuote = fees + } +} + +// NoVerifyFees will switch off transaction fee verification and rely on +// mAPI / node verification when the transaction is broadcast. +func NoVerifyFees() VerifyOpt { + return func(opts *verifyOptions) { + opts.fees = false + opts.feeQuote = nil + } +} + +// VerifyScript will ensure the scripts provided in the transaction are valid. +func VerifyScript() VerifyOpt { + return func(opts *verifyOptions) { + opts.script = true + } +} + +// NoVerifyScript will switch off script verification and rely on +// mAPI / node verification when the tx is broadcast. +func NoVerifyScript() VerifyOpt { + return func(opts *verifyOptions) { + opts.script = false + } +} + +// NoVerifySPV will turn off any spv validation for merkle proofs +// and script validation. This is a helper method that is equivalent to +// NoVerifyProofs && NoVerifyScripts. +func NoVerifySPV() VerifyOpt { + return func(opts *verifyOptions) { + opts.proofs = false + opts.script = false + } +} + +// VerifySPV will turn on spv validation for merkle proofs +// and script validation. This is a helper method that is equivalent to +// VerifyProofs && VerifyScripts. +func VerifySPV() VerifyOpt { + return func(opts *verifyOptions) { + opts.proofs = true + opts.script = true + } +} + +// A PaymentVerifier is an interface used to complete Simple Payment Verification (SPV) +// in conjunction with a Merkle Proof. +// +// The implementation of bc.BlockHeaderChain which is supplied will depend on the client +// you are using, some may return a HeaderJSON response others may return the blockhash. +type PaymentVerifier interface { + VerifyPayment(ctx context.Context, p *Payment, opts ...VerifyOpt) error + MerkleProofVerifier +} + +// MerkleProofVerifier interfaces the verification of Merkle Proofs. +type MerkleProofVerifier interface { + VerifyMerkleProof(context.Context, []byte) (*MerkleProofValidation, error) + VerifyMerkleProofJSON(context.Context, *bc.MerkleProof) (bool, bool, error) +} + +type verifier struct { + // BlockHeaderChain will be set when an implementation returning a bc.BlockHeader type is provided. + bhc bc.BlockHeaderChain + opts *verifyOptions +} + +// NewPaymentVerifier creates a new spv.PaymentVerifer with the bc.BlockHeaderChain provided. +// If no BlockHeaderChain implementation is provided, the setup will return an error. +// +// opts control the global behaviour of the verifier and all options are enabled by default, they are: +// - ancestry verification (proofs checked etc) +// - fees checked, ensuring the root tx covers enough fees +// - script verification which checks the script is correct (not currently implemented). +func NewPaymentVerifier(bhc bc.BlockHeaderChain, opts ...VerifyOpt) (PaymentVerifier, error) { + o := &verifyOptions{ + proofs: true, + fees: false, + script: true, + } + for _, opt := range opts { + opt(o) + } + if o.proofs && bhc == nil { + return nil, errors.New("at least one blockchain header implementation should be returned") + } + return &verifier{bhc: bhc, opts: o}, nil +} + +// NewMerkleProofVerifier creates a new spv.MerkleProofVerifer with the bc.BlockHeaderChain provided. +// If no BlockHeaderChain implementation is provided, the setup will return an error. +func NewMerkleProofVerifier(bhc bc.BlockHeaderChain) (MerkleProofVerifier, error) { + return NewPaymentVerifier(bhc) +} diff --git a/vendor/github.com/libsv/go-bc/spv/verifymerkleproof.go b/vendor/github.com/libsv/go-bc/spv/verifymerkleproof.go new file mode 100644 index 0000000..e4f00bc --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/verifymerkleproof.go @@ -0,0 +1,328 @@ +package spv + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + + "github.com/libsv/go-bt/v2" + + "github.com/libsv/go-bc" +) + +const ( + txOrIDFlag byte = 1 << iota // 1 << 0 which is 00000001 + targetTypeFlag1 // 1 << 1 which is 00000010 + targetTypeFlag2 // 1 << 2 which is 00000100 + proofTypeFlag // 1 << 3 which is 00001000 + compositeFlag // 1 << 4 which is 00010000 + + targetTypeFlags = targetTypeFlag1 | targetTypeFlag2 +) + +// MerkleProofValidation is a wrapper for the response of a validation operation. +type MerkleProofValidation struct { + TxID string + Valid bool + IsLastInTree bool +} + +// VerifyMerkleProof verifies a Merkle Proof in standard byte format. +func (v *verifier) VerifyMerkleProof(ctx context.Context, proof []byte) (*MerkleProofValidation, error) { + mpb, err := parseBinaryMerkleProof(proof) + if err != nil { + return nil, err + } + + err = validateTxOrID(mpb.flags, mpb.txOrID) + if err != nil { + return nil, err + } + + txid, err := txidFromTxOrID(mpb.txOrID) + if err != nil { + return nil, err + } + + response := &MerkleProofValidation{ + TxID: txid, + } + + var merkleRoot string + switch mpb.flags & targetTypeFlags { + // if bits 1 and 2 of flags are NOT set, target should contain a block hash (32 bytes) + case 0: + // The `target` field contains a block hash + blockHeader, err := v.bhc.BlockHeader(ctx, mpb.target) + if err != nil { + return response, err + } + + merkleRoot = blockHeader.HashMerkleRootStr() + + // if bit 2 of flags is set, target should contain a merkle root (32 bytes) + case 4: + // the `target` field contains a merkle root + merkleRoot = mpb.target + + // if bit 1 of flags is set, target should contain a block header (80 bytes) + case 2: + // The `target` field contains a block header + var err error + merkleRoot, err = bc.ExtractMerkleRootFromBlockHeader(mpb.target) + if err != nil { + return response, err + } + + default: + return response, ErrInvalidMerkleFlags + } + + if mpb.flags&proofTypeFlag == 1 { + return response, ErrInvalidMerkleFlags + } + + if mpb.flags&compositeFlag == 1 { + return response, ErrInvalidMerkleFlags // composite proof type not supported + } + + if txid == "" { + return response, ErrMissingTxidInProof + } + + if merkleRoot == "" { + return response, ErrMissingRootInProof + } + + valid, isLastInTree, err := verifyProof(txid, merkleRoot, mpb.index, mpb.nodes) + + return &MerkleProofValidation{ + TxID: txid, + Valid: valid, + IsLastInTree: isLastInTree, + }, err +} + +// VerifyMerkleProofJSON verifies a Merkle Proof in standard JSON format. +func (v *verifier) VerifyMerkleProofJSON(ctx context.Context, proof *bc.MerkleProof) (bool, bool, error) { + + txid, err := txidFromTxOrID(proof.TxOrID) + if err != nil { + return false, false, err + } + + var merkleRoot string + if proof.TargetType == "" || proof.TargetType == "hash" { + // The `target` field contains a block hash + + if len(proof.Target) != 64 { + return false, false, errors.New("invalid target field") + } + + blockHeader, err := v.bhc.BlockHeader(ctx, proof.Target) + if err != nil { + return false, false, err + } + merkleRoot = blockHeader.HashMerkleRootStr() + + } else if proof.TargetType == "header" && len(proof.Target) == 160 { + // The `target` field contains a block header + var err error + merkleRoot, err = bc.ExtractMerkleRootFromBlockHeader(proof.Target) + if err != nil { + return false, false, err + } + + } else if proof.TargetType == "merkleRoot" && len(proof.Target) == 64 { + // the `target` field contains a merkle root + merkleRoot = proof.Target + + } else { + return false, false, errors.New("invalid TargetType or target field") + } + + if proof.ProofType != "" && proof.ProofType != "branch" { + return false, false, errors.New("only merkle branch supported in this version") // merkle tree proof type not supported + } + + if proof.Composite { // OR if (proof.composite && proof.composite != false) + return false, false, errors.New("only single proof supported in this version") // composite proof type not supported + } + + if txid == "" { + return false, false, errors.New("txid missing") + } + + if merkleRoot == "" { + return false, false, errors.New("merkleRoot missing") + } + + return verifyProof(txid, merkleRoot, proof.Index, proof.Nodes) +} + +func verifyProof(c, merkleRoot string, index uint64, nodes []string) (bool, bool, error) { + isLastInTree := true + + for _, p := range nodes { + // Check if the node is the left or the right child + cIsLeft := index%2 == 0 + + // Check for duplicate hash - this happens if the node (p) is + // the last element of an uneven merkle tree layer + if p == "*" { + if !cIsLeft { // this shouldn't happen... + return false, false, ErrInvalidNodes + } + p = c + } + + // This check fails at least once if it's not the last element + if cIsLeft && c != p { + isLastInTree = false + } + + var err error + // Calculate the parent node + if cIsLeft { + // Concatenate left leaf (c) with right leaf (p) + c, err = bc.MerkleTreeParentStr(c, p) + if err != nil { + return false, false, err + } + } else { + // Concatenate left leaf (p) with right leaf (c) + c, err = bc.MerkleTreeParentStr(p, c) + if err != nil { + return false, false, err + } + } + + // We need integer division here with remainder dropped. + index = index / 2 + } + + // c is now the calculated merkle root + return c == merkleRoot, isLastInTree, nil +} + +func validateTxOrID(flags byte, txOrID string) error { + // The `txOrId` field contains a full transaction + if len(txOrID) > 64 && flags&txOrIDFlag == 0 { + return errors.New("expecting txid but got tx") + } + + // The `txOrId` field contains a transaction ID + if len(txOrID) == 64 && flags&txOrIDFlag == 1 { + return errors.New("expecting tx but got txid") + } + + return nil +} + +func txidFromTxOrID(txOrID string) (string, error) { + + // The `txOrId` field contains a transaction ID + if len(txOrID) == 64 { + return txOrID, nil + } + + // The `txOrId` field contains a full transaction + if len(txOrID) > 64 { + tx, err := bt.NewTxFromString(txOrID) + if err != nil { + return "", err + } + + return tx.TxID(), nil + } + + return "", errors.New("invalid txOrId length - must be at least 64 chars (32 bytes)") +} + +type merkleProofBinary struct { + flags byte + index uint64 + txOrID string + target string + nodes []string +} + +func parseBinaryMerkleProof(proof []byte) (*merkleProofBinary, error) { + mpb := &merkleProofBinary{} + + var offset int + + // flags is first byte + mpb.flags = proof[offset] + offset++ + + // index is the next varint after the 1st byte + index, size := bt.NewVarIntFromBytes(proof[offset:]) + mpb.index = uint64(index) + offset += size + + var txLength bt.VarInt + // if bit 1 of flags is NOT set, txOrId should contain txid (= 32 bytes) + if mpb.flags&1 == 0 { + txLength = 32 + } + + // if bit 1 of flags is set, txOrId should contain tx hex (> 32 bytes) + if mpb.flags&1 == 1 { + // txLength is the next varint after the 1st byte + index size + txLength, size = bt.NewVarIntFromBytes(proof[offset:]) + offset += size + if txLength <= 32 { + return nil, errors.New("invalid tx length (should be greater than 32 bytes)") + } + } + + // txOrID is the next txLength bytes after 1st byte + index size (+ txLength size) + mpb.txOrID = hex.EncodeToString(bt.ReverseBytes(proof[offset : offset+int(txLength)])) + offset += int(txLength) + + switch mpb.flags & targetTypeFlags { + // if bits 1 and 2 of flags are NOT set, target should contain a block hash (32 bytes) + // if bit 2 of flags is set, target should contain a merkle root (32 bytes) + case 0, 4: + mpb.target = hex.EncodeToString(bt.ReverseBytes(proof[offset : offset+32])) + offset += 32 + + // if bit 1 of flags is set, target should contain a block header (80 bytes) + case 2: + mpb.target = hex.EncodeToString(bt.ReverseBytes(proof[offset : offset+80])) + offset += 80 + + default: + return nil, ErrInvalidMerkleFlags + } + + nodeCount, size := bt.NewVarIntFromBytes(proof[offset:]) + offset += size + + if mpb.index >= 1< Date: Mon, 1 Aug 2022 18:41:13 +1200 Subject: [PATCH 4/8] Rename all instances of `PaymentRequest` to `PaymentTerms` & refactoring to place each of the three dpp message type structs in their own folders --- merchant.go => beneficiary.go | 0 mocks/mocks.go | 2 +- mocks/payment_request_service.go | 66 +++++------ modes/hybridmode/hybrid_payment.go | 18 +++ modes/hybridmode/hybrid_payment_ack.go | 17 +++ modes/hybridmode/hybrid_payment_terms.go | 42 +++++++ .../native_types.go | 13 ++- payment.go | 45 +++++--- payment_ack.go | 46 +------- payment_request.go | 108 ------------------ payment_terms.go | 71 ++++++++++++ payment_test.go | 7 +- 12 files changed, 231 insertions(+), 204 deletions(-) rename merchant.go => beneficiary.go (100%) create mode 100644 modes/hybridmode/hybrid_payment.go create mode 100644 modes/hybridmode/hybrid_payment_ack.go create mode 100644 modes/hybridmode/hybrid_payment_terms.go rename destinations.go => nativetypes/native_types.go (54%) delete mode 100644 payment_request.go create mode 100644 payment_terms.go diff --git a/merchant.go b/beneficiary.go similarity index 100% rename from merchant.go rename to beneficiary.go diff --git a/mocks/mocks.go b/mocks/mocks.go index 6278b29..5fa99db 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -2,4 +2,4 @@ package mocks //go:generate moq -pkg mocks -out payment_writer.go ../ PaymentWriter //go:generate moq -pkg mocks -out payment_service.go ../ PaymentService -//go:generate moq -pkg mocks -out payment_request_service.go ../ PaymentRequestService +//go:generate moq -pkg mocks -out payment_request_service.go ../ PaymentTermsService diff --git a/mocks/payment_request_service.go b/mocks/payment_request_service.go index 54d8dbd..c4fc595 100644 --- a/mocks/payment_request_service.go +++ b/mocks/payment_request_service.go @@ -9,73 +9,73 @@ import ( "sync" ) -// Ensure, that PaymentRequestServiceMock does implement dpp.PaymentRequestService. +// Ensure, that PaymentTermsServiceMock does implement dpp.PaymentTermsService. // If this is not the case, regenerate this file with moq. -var _ dpp.PaymentRequestService = &PaymentRequestServiceMock{} +var _ dpp.PaymentTermsService = &PaymentTermsServiceMock{} -// PaymentRequestServiceMock is a mock implementation of dpp.PaymentRequestService. +// PaymentTermsServiceMock is a mock implementation of dpp.PaymentTermsService. // -// func TestSomethingThatUsesPaymentRequestService(t *testing.T) { +// func TestSomethingThatUsesPaymentTermsService(t *testing.T) { // -// // make and configure a mocked dpp.PaymentRequestService -// mockedPaymentRequestService := &PaymentRequestServiceMock{ -// PaymentRequestFunc: func(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentRequest, error) { -// panic("mock out the PaymentRequest method") +// // make and configure a mocked dpp.PaymentTermsService +// mockedPaymentTermsService := &PaymentTermsServiceMock{ +// PaymentTermsFunc: func(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) { +// panic("mock out the PaymentTerms method") // }, // } // -// // use mockedPaymentRequestService in code that requires dpp.PaymentRequestService +// // use mockedPaymentTermsService in code that requires dpp.PaymentTermsService // // and then make assertions. // // } -type PaymentRequestServiceMock struct { - // PaymentRequestFunc mocks the PaymentRequest method. - PaymentRequestFunc func(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentTerms, error) +type PaymentTermsServiceMock struct { + // PaymentTermsFunc mocks the PaymentTerms method. + PaymentTermsFunc func(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) // calls tracks calls to the methods. calls struct { - // PaymentRequest holds details about calls to the PaymentRequest method. - PaymentRequest []struct { + // PaymentTerms holds details about calls to the PaymentTerms method. + PaymentTerms []struct { // Ctx is the ctx argument value. Ctx context.Context // Args is the args argument value. - Args dpp.PaymentRequestArgs + Args dpp.PaymentTermsArgs } } - lockPaymentRequest sync.RWMutex + lockPaymentTerms sync.RWMutex } -// PaymentRequest calls PaymentRequestFunc. -func (mock *PaymentRequestServiceMock) PaymentRequest(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentTerms, error) { - if mock.PaymentRequestFunc == nil { - panic("PaymentRequestServiceMock.PaymentRequestFunc: method is nil but PaymentRequestService.PaymentRequest was just called") +// PaymentTerms calls PaymentTermsFunc. +func (mock *PaymentTermsServiceMock) PaymentTerms(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) { + if mock.PaymentTermsFunc == nil { + panic("PaymentTermsServiceMock.PaymentTermsFunc: method is nil but PaymentTermsService.PaymentTerms was just called") } callInfo := struct { Ctx context.Context - Args dpp.PaymentRequestArgs + Args dpp.PaymentTermsArgs }{ Ctx: ctx, Args: args, } - mock.lockPaymentRequest.Lock() - mock.calls.PaymentRequest = append(mock.calls.PaymentRequest, callInfo) - mock.lockPaymentRequest.Unlock() - return mock.PaymentRequestFunc(ctx, args) + mock.lockPaymentTerms.Lock() + mock.calls.PaymentTerms = append(mock.calls.PaymentTerms, callInfo) + mock.lockPaymentTerms.Unlock() + return mock.PaymentTermsFunc(ctx, args) } -// PaymentRequestCalls gets all the calls that were made to PaymentRequest. +// PaymentTermsCalls gets all the calls that were made to PaymentTerms. // Check the length with: -// len(mockedPaymentRequestService.PaymentRequestCalls()) -func (mock *PaymentRequestServiceMock) PaymentRequestCalls() []struct { +// len(mockedPaymentTermsService.PaymentTermsCalls()) +func (mock *PaymentTermsServiceMock) PaymentTermsCalls() []struct { Ctx context.Context - Args dpp.PaymentRequestArgs + Args dpp.PaymentTermsArgs } { var calls []struct { Ctx context.Context - Args dpp.PaymentRequestArgs + Args dpp.PaymentTermsArgs } - mock.lockPaymentRequest.RLock() - calls = mock.calls.PaymentRequest - mock.lockPaymentRequest.RUnlock() + mock.lockPaymentTerms.RLock() + calls = mock.calls.PaymentTerms + mock.lockPaymentTerms.RUnlock() return calls } diff --git a/modes/hybridmode/hybrid_payment.go b/modes/hybridmode/hybrid_payment.go new file mode 100644 index 0000000..baafa6a --- /dev/null +++ b/modes/hybridmode/hybrid_payment.go @@ -0,0 +1,18 @@ +package hybridmode + +import "github.com/libsv/go-bc/spv" + + +// Payment includes data required for hybridmode payment mode. +type Payment struct { + // OptionID ID of chosen payment options + OptionID string `json:"optionId"` + // Transactions A list of valid, signed Bitcoin transactions that fully pays the PaymentTerms. + // The transaction is hex-encoded and must NOT be prefixed with “0x”. + // The order of transactions should match the order from PaymentTerms for this mode. + Transactions []string `json:"transactions"` + // Ancestors a map of txid to ancestry transaction info for the transactions in above + // each ancestor contains the TX together with the MerkleProof needed when SPVRequired is true. + // See: https://tsc.bitcoinassociation.net/standards/transaction-ancestors/ + Ancestors map[string]spv.TSCAncestryJSON `json:"ancestors"` +} diff --git a/modes/hybridmode/hybrid_payment_ack.go b/modes/hybridmode/hybrid_payment_ack.go new file mode 100644 index 0000000..f41a858 --- /dev/null +++ b/modes/hybridmode/hybrid_payment_ack.go @@ -0,0 +1,17 @@ +package hybridmode + + +// PeerChannelData holds peer channel information for subscribing to and reading from a peer channel. +type PeerChannelData struct { + Host string `json:"host"` + Path string `json:"path"` + ChannelID string `json:"channel_id"` + Token string `json:"token"` +} + + +// PaymentACK includes data required for hybridmode payment mode. +type PaymentACK struct { + TransactionIds []string `json:"transactionIds"` + PeerChannel *PeerChannelData `json:"peerChannel"` +} diff --git a/modes/hybridmode/hybrid_payment_terms.go b/modes/hybridmode/hybrid_payment_terms.go new file mode 100644 index 0000000..17d0855 --- /dev/null +++ b/modes/hybridmode/hybrid_payment_terms.go @@ -0,0 +1,42 @@ +package hybridmode + +import ( + "github.com/libsv/go-dpp/nativetypes" +) + +// These structures are defined in the TSC spec: +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + + +// Policies An object containing some policy information like fees or whether Ancestors are +// required in the `Payment`. +type Policies struct { + // FeeRate defines the amount of fees a users wallet should add to the payment + // when submitting their final payments. + FeeRate map[string]map[string]int `json:"fees,omitempty"` + SPVRequired bool `json:"SPVRequired,omitempty"` + LockTime uint32 `json:"lockTime,omitempty"` +} + +// Inputs provides options of different arrays of input script types. +// Currently, only "native" type input are supported. +type Inputs struct { + NativeOutputs []nativetypes.NativeInput `json:"native"` +} + +// Outputs provides options of different arrays of output script types. +// Currently, only "native" type outputs are supported. +type Outputs struct { + NativeOutputs []nativetypes.NativeOutput `json:"native"` +} + +// TransactionTerms a single definition of requested transaction format for the standard payment mode: +// "ef63d9775da5" in the DPP TSC spec. +type TransactionTerms struct { + Outputs Outputs `json:"outputs"` + Inputs Inputs `json:"inputs,omitempty"` + Policies *Policies `json:"policies"` +} + +// PaymentTerms message used in DPP TSC spec. for the `PaymentTerms` message. +type PaymentTerms map[string]map[string][]TransactionTerms diff --git a/destinations.go b/nativetypes/native_types.go similarity index 54% rename from destinations.go rename to nativetypes/native_types.go index 56ede9a..d66dc3a 100644 --- a/destinations.go +++ b/nativetypes/native_types.go @@ -1,4 +1,4 @@ -package dpp +package nativetypes import ( "github.com/libsv/go-bt/v2/bscript" @@ -13,3 +13,14 @@ type NativeOutput struct { // Description, an optional description such as "tip" or "sales tax". Maximum length is 100 chars. Description string `json:"description,omitempty" example:"paymentReference 123456"` } + + +// NativeInput a way of declaring requirements for the inputs which should be used. It is "native" to distinguish it +// from a token input in the hybridmode payment mode. +type NativeInput struct { + ScriptSig string `json:"scriptSig" binding:"required"` // string. required. + TxID string `json:"txid" binding:"required"` // string. required. + Vout uint32 `json:"vout" binding:"required"` // integer. required. + Value uint64 `json:"value" binding:"required"` // integer. required. + NSequence int `json:"nSequence,omitempty"` // number. optional. +} diff --git a/payment.go b/payment.go index 17a71c6..47db75d 100644 --- a/payment.go +++ b/payment.go @@ -1,27 +1,14 @@ package dpp import ( - "github.com/libsv/go-bc/spv" + "context" + "github.com/libsv/go-dpp/modes/hybridmode" validator "github.com/theflyingcodr/govalidator" ) // These structures are defined in the TSC spec: // See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol -// HybridPaymentModePayment includes data required for hybrid payment mode. -type HybridPaymentModePayment struct { - // OptionID ID of chosen payment options - OptionID string `json:"optionId"` - // Transactions A list of valid, signed Bitcoin transactions that fully pays the PaymentTerms. - // The transaction is hex-encoded and must NOT be prefixed with “0x”. - // The order of transactions should match the order from PaymentTerms for this mode. - Transactions []string `json:"transactions"` - // Ancestors a map of txid to ancestry transaction info for the transactions in above - // each ancestor contains the TX together with the MerkleProof needed when SPVRequired is true. - // See: https://tsc.bitcoinassociation.net/standards/transaction-ancestors/ - Ancestors map[string]spv.TSCAncestryJSON `json:"ancestors"` -} - // Originator Data about payer. This data might be needed in many cases, e.g. tracking data for later loyalty // points processing etc. type Originator struct { @@ -38,10 +25,10 @@ type Originator struct { // Payment is a Payment message used in BIP270. // See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#payment type Payment struct { - // ModeID chosen from possible modes of PaymentTerms. + // ModeID chosen from possible messages of PaymentTerms. ModeID string `json:"modeId" binding:"required" example:"ef63d9775da5"` - // Mode Object with data required by specific mode, e.g. HybridPaymentMode - Mode HybridPaymentModePayment `json:"mode" binding:"required"` + // Mode Object with data required by specific mode, e.g. HybridPaymentTerms + Mode hybridmode.Payment `json:"mode" binding:"required"` // Originator Data about payer. This data might be needed in many cases, e.g. refund, tract data for later loyalty points processing etc. Originator Originator `json:"originator"` // Transaction A single valid, signed Bitcoin transaction that fully pays the PaymentTerms. This field is deprecated. @@ -65,3 +52,25 @@ func (p Payment) Validate() error { type ProofCallback struct { Token string `json:"token"` } + +// PaymentCreateArgs identifies the paymentID used for the payment. +type PaymentCreateArgs struct { + PaymentID string `param:"paymentID"` +} + +// Validate will ensure that the PaymentCreateArgs are supplied and correct. +func (p PaymentCreateArgs) Validate() error { + return validator.New(). + Validate("paymentID", validator.NotEmpty(p.PaymentID)). + Err() +} + +// PaymentService enforces business rules when creating payments. +type PaymentService interface { + PaymentCreate(ctx context.Context, args PaymentCreateArgs, req Payment) (*PaymentACK, error) +} + +// PaymentWriter will write a payment to a data store. +type PaymentWriter interface { + PaymentCreate(ctx context.Context, args PaymentCreateArgs, req Payment) (*PaymentACK, error) +} diff --git a/payment_ack.go b/payment_ack.go index 038dbf8..6184ead 100644 --- a/payment_ack.go +++ b/payment_ack.go @@ -1,15 +1,11 @@ package dpp import ( - "context" - validator "github.com/theflyingcodr/govalidator" + "github.com/libsv/go-dpp/modes/hybridmode" ) -// HybridPaymentModeACK includes data required for hybrid payment mode. -type HybridPaymentModeACK struct { - TransactionIds []string `json:"transactionIds"` - PeerChannel *PeerChannelData `json:"peerChannel"` -} +// These structures are defined in the TSC spec: +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol // PaymentACK message used in the TSC DPP spec. // See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol/#PaymentModes @@ -17,9 +13,9 @@ type PaymentACK struct { // ModeID the chosen mode. ModeID string `json:"modeId" binding:"required" example:"ef63d9775da5"` // Mode data required by specific payment mode - Mode *HybridPaymentModeACK `json:"mode"` - PeerChannel *PeerChannelData `json:"peerChannel"` - RedirectURL string `json:"redirectUrl"` + Mode *hybridmode.PaymentACK `json:"mode"` + PeerChannel *hybridmode.PeerChannelData `json:"peerChannel"` + RedirectURL string `json:"redirectUrl"` // Memo may contain information about why there was an error. This field is poorly defined until // error reporting is more standardised. @@ -30,33 +26,3 @@ type PaymentACK struct { // the transaction was not accepted until further numbers are defined and standardised. Error int `json:"error,omitempty"` } - -// PeerChannelData holds peer channel information for subscribing to and reading from a peer channel. -type PeerChannelData struct { - Host string `json:"host"` - Path string `json:"path"` - ChannelID string `json:"channel_id"` - Token string `json:"token"` -} - -// PaymentCreateArgs identifies the paymentID used for the payment. -type PaymentCreateArgs struct { - PaymentID string `param:"paymentID"` -} - -// Validate will ensure that the PaymentCreateArgs are supplied and correct. -func (p PaymentCreateArgs) Validate() error { - return validator.New(). - Validate("paymentID", validator.NotEmpty(p.PaymentID)). - Err() -} - -// PaymentService enforces business rules when creating payments. -type PaymentService interface { - PaymentCreate(ctx context.Context, args PaymentCreateArgs, req Payment) (*PaymentACK, error) -} - -// PaymentWriter will write a payment to a data store. -type PaymentWriter interface { - PaymentCreate(ctx context.Context, args PaymentCreateArgs, req Payment) (*PaymentACK, error) -} diff --git a/payment_request.go b/payment_request.go deleted file mode 100644 index 614796a..0000000 --- a/payment_request.go +++ /dev/null @@ -1,108 +0,0 @@ -package dpp - -import ( - "context" -) - -// These structures are defined in the TSC spec: -// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol - -// Policies An object containing some policy information like fees or whether Ancestors are -// required in the `Payment`. -type Policies struct { - // FeeRate defines the amount of fees a users wallet should add to the payment - // when submitting their final payments. - FeeRate map[string]map[string]int `json:"fees,omitempty"` - SPVRequired bool `json:"SPVRequired,omitempty"` - LockTime uint32 `json:"lockTime,omitempty"` -} - -// NativeInput a way of declaring requirements for the inputs which should be used. -type NativeInput struct { - ScriptSig string `json:"scriptSig" binding:"required"` // string. required. - TxID string `json:"txid" binding:"required"` // string. required. - Vout uint32 `json:"vout" binding:"required"` // integer. required. - Value uint64 `json:"value" binding:"required"` // integer. required. - NSequence int `json:"nSequence,omitempty"` // number. optional. -} - -// Inputs provides options of different arrays of input script types. -// Currently, only "native" type input are supported. -type Inputs struct { - NativeOutputs []NativeInput `json:"native"` -} - -// Outputs provides options of different arrays of output script types. -// Currently, only "native" type outputs are supported. -type Outputs struct { - NativeOutputs []NativeOutput `json:"native"` -} - -// TransactionTerms a single definition of requested transaction format for the standard payment mode: -// "ef63d9775da5" in the DPP TSC spec. -type TransactionTerms struct { - Outputs Outputs `json:"outputs"` - Inputs Inputs `json:"inputs,omitempty"` - Policies *Policies `json:"policies"` -} - -// PaymentModes message used in DPP TSC spec. -// At present we will strictly only allow the "standard" mode of payment with native bitcoins (satoshis). Handling -// of tokens is left for a later date. -type PaymentModes struct { - HybridPaymentMode map[string]map[string][]TransactionTerms `json:"ef63d9775da5"` -} - -// PaymentTerms message as defined in the DPP T$C spec. -type PaymentTerms struct { - // Network Always set to "bitcoin" (but seems to be set to 'bitcoin-sv' - // outside bip270 spec, see https://handcash.github.io/handcash-merchant-integration/#/merchant-payments) - // {enum: bitcoin, bitcoin-sv, test} - // Required. - Network string `json:"network" binding:"required" example:"mainnet" enums:"mainnet,testnet,stn,regtest"` - // Version version of DPP TSC spec. - // Required. - Version string `json:"version" binding:"required" example:"1.0"` - // Outputs an array of outputs. DEPRECATED but included for backward compatibility. - // Optional. - Outputs []NativeOutput `json:"outputs,omitempty"` - // CreationTimestamp Unix timestamp (seconds since 1-Jan-1970 UTC) when the PaymentRequest was created. - // Required. - CreationTimestamp int64 `json:"creationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648163657"` - // ExpirationTimestamp Unix timestamp (UTC) after which the PaymentRequest should be considered invalid. - // Optional. - ExpirationTimestamp int64 `json:"expirationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648164657"` - // PaymentURL secure HTTPS location where a Payment message (see below) will be sent to obtain a PaymentACK. - // Maximum length is 4000 characters - PaymentURL string `json:"paymentUrl" binding:"required" example:"http://localhost:3443/api/v1/payment/123456"` - // Memo note that should be displayed to the customer, explaining what this PaymentRequest is for. - // Maximum length is 50 characters. - // Optional. - Memo string `json:"memo,omitempty" example:"invoice number 123456"` - // Beneficiary Arbitrary data that may be used by the payment host to identify the PaymentRequest - // May be omitted if the payment host does not need to associate Payments with PaymentRequest - // or if they associate each PaymentRequest with a separate payment address. - // Maximum length is 10000 characters. - // Optional. - Beneficiary *Beneficiary `json:"beneficiary,omitempty"` - // Modes TSC payment modes specified by ID (and well defined) modes customer can choose to pay - // A key-value map. required field but not if legacy BIP270 outputs are provided - Modes *PaymentModes `json:"modes"` -} - -// PaymentRequestArgs are request arguments that can be passed to the service. -type PaymentRequestArgs struct { - // PaymentID is an identifier for an invoice. - PaymentID string `param:"paymentID"` -} - -// PaymentRequestService can be implemented to enforce business rules -// and process in order to fulfil a PaymentRequest. -type PaymentRequestService interface { - PaymentRequestReader -} - -// PaymentRequestReader will return a new payment request. -type PaymentRequestReader interface { - PaymentRequest(ctx context.Context, args PaymentRequestArgs) (*PaymentTerms, error) -} diff --git a/payment_terms.go b/payment_terms.go new file mode 100644 index 0000000..f52c58c --- /dev/null +++ b/payment_terms.go @@ -0,0 +1,71 @@ +package dpp + +import ( + "context" + "github.com/libsv/go-dpp/modes/hybridmode" + "github.com/libsv/go-dpp/nativetypes" +) + +// These structures are defined in the TSC spec: +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +// PaymentTermsModes message used in DPP TSC spec. for the `PaymentTerms` message. +type PaymentTermsModes struct { + // Hybrid contains a key value map of possible payment terms modalities - currently there is only one option: + // `HybridPaymentMode` with BRFCID: "ef63d9775da5". + Hybrid hybridmode.PaymentTerms `json:"ef63d9775da5"` +} + +// PaymentTerms message as defined in the DPP T$C spec. +type PaymentTerms struct { + // Network Always set to "bitcoin" (but seems to be set to 'bitcoin-sv' + // outside bip270 spec, see https://handcash.github.io/handcash-merchant-integration/#/merchant-payments) + // {enum: bitcoin, bitcoin-sv, test} + // Required. + Network string `json:"network" binding:"required" example:"mainnet" enums:"mainnet,testnet,stn,regtest"` + // Version version of DPP TSC spec. + // Required. + Version string `json:"version" binding:"required" example:"1.0"` + // Outputs an array of outputs. DEPRECATED but included for backward compatibility. + // Optional. + Outputs []nativetypes.NativeOutput `json:"outputs,omitempty"` + // CreationTimestamp Unix timestamp (seconds since 1-Jan-1970 UTC) when the PaymentTerms were created. + // Required. + CreationTimestamp int64 `json:"creationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648163657"` + // ExpirationTimestamp Unix timestamp (UTC) after which the PaymentTerms should be considered invalid. + // Optional. + ExpirationTimestamp int64 `json:"expirationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648164657"` + // PaymentURL secure HTTPS location where a Payment message (see below) will be sent to obtain a PaymentACK. + // Maximum length is 4000 characters + PaymentURL string `json:"paymentUrl" binding:"required" example:"http://localhost:3443/api/v1/payment/123456"` + // Memo note that should be displayed to the customer, explaining what these PaymentTerms are for. + // Maximum length is 50 characters. + // Optional. + Memo string `json:"memo,omitempty" example:"invoice number 123456"` + // Beneficiary Arbitrary data that may be used by the payment host to identify the PaymentTerms + // May be omitted if the payment host does not need to associate Payments with PaymentTerms + // or if they associate each PaymentTerms with a separate payment address. + // Maximum length is 10000 characters. + // Optional. + Beneficiary *Beneficiary `json:"beneficiary,omitempty"` + // PaymentTermsModes TSC payment messages specified by ID (and well defined) messages customer can choose to pay + // A key-value map. required field but not if legacy BIP270 outputs are provided + Modes *PaymentTermsModes`json:"modes"` +} + +// PaymentTermsArgs are request arguments that can be passed to the service. +type PaymentTermsArgs struct { + // PaymentID is an identifier for an invoice. + PaymentID string `param:"paymentID"` +} + +// PaymentTermsService can be implemented to enforce business rules +// and process in order to fulfil PaymentTerms. +type PaymentTermsService interface { + PaymentTermsReader +} + +// PaymentTermsReader will return a new payment request. +type PaymentTermsReader interface { + PaymentTerms(ctx context.Context, args PaymentTermsArgs) (*PaymentTerms, error) +} diff --git a/payment_test.go b/payment_test.go index 77456ce..1659a4a 100644 --- a/payment_test.go +++ b/payment_test.go @@ -1,6 +1,7 @@ package dpp import ( + "github.com/libsv/go-dpp/modes/hybridmode" "testing" "github.com/matryer/is" @@ -15,7 +16,7 @@ func TestPaymentCreate_Validate(t *testing.T) { "valid request should return no errors": { req: Payment{ ModeID: "ef63d9775da5", - Mode: HybridPaymentModePayment{ + Mode: hybridmode.Payment{ OptionID: "choiceID0", Transactions: []string{"tx1", "tx2"}, }, @@ -32,7 +33,7 @@ func TestPaymentCreate_Validate(t *testing.T) { }, }, "mode id missing should error": { req: Payment{ - Mode: HybridPaymentModePayment{ + Mode: hybridmode.Payment{ OptionID: "choiceID0", Transactions: []string{ "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000", @@ -60,7 +61,7 @@ func TestPaymentCreate_Validate(t *testing.T) { }, "mode missing transaction field should error": { req: Payment{ ModeID: "ef63d9775da5", - Mode: HybridPaymentModePayment{ + Mode: hybridmode.Payment{ OptionID: "choiceID0", }, }, From 0ceb22643da5d7ff7759e2316f73773dcaff022c Mon Sep 17 00:00:00 2001 From: Roger Taylor <32600614+rt121212121@users.noreply.github.com> Date: Mon, 8 Aug 2022 18:27:02 +1200 Subject: [PATCH 5/8] Update for secure payment terms. (#70) --- mocks/payment_request_service.go | 76 +++++++++++++++++++++++++++----- payment_terms.go | 5 ++- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/mocks/payment_request_service.go b/mocks/payment_request_service.go index c4fc595..0c8604d 100644 --- a/mocks/payment_request_service.go +++ b/mocks/payment_request_service.go @@ -5,6 +5,7 @@ package mocks import ( "context" + "github.com/libsv/go-bk/envelope" "github.com/libsv/go-dpp" "sync" ) @@ -15,23 +16,29 @@ var _ dpp.PaymentTermsService = &PaymentTermsServiceMock{} // PaymentTermsServiceMock is a mock implementation of dpp.PaymentTermsService. // -// func TestSomethingThatUsesPaymentTermsService(t *testing.T) { +// func TestSomethingThatUsesPaymentTermsService(t *testing.T) { // -// // make and configure a mocked dpp.PaymentTermsService -// mockedPaymentTermsService := &PaymentTermsServiceMock{ -// PaymentTermsFunc: func(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) { -// panic("mock out the PaymentTerms method") -// }, -// } +// // make and configure a mocked dpp.PaymentTermsService +// mockedPaymentTermsService := &PaymentTermsServiceMock{ +// PaymentTermsFunc: func(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) { +// panic("mock out the PaymentTerms method") +// }, +// PaymentTermsSecureFunc: func(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) { +// panic("mock out the PaymentTermsSecure method") +// }, +// } // -// // use mockedPaymentTermsService in code that requires dpp.PaymentTermsService -// // and then make assertions. +// // use mockedPaymentTermsService in code that requires dpp.PaymentTermsService +// // and then make assertions. // -// } +// } type PaymentTermsServiceMock struct { // PaymentTermsFunc mocks the PaymentTerms method. PaymentTermsFunc func(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) + // PaymentTermsSecureFunc mocks the PaymentTermsSecure method. + PaymentTermsSecureFunc func(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) + // calls tracks calls to the methods. calls struct { // PaymentTerms holds details about calls to the PaymentTerms method. @@ -41,8 +48,16 @@ type PaymentTermsServiceMock struct { // Args is the args argument value. Args dpp.PaymentTermsArgs } + // PaymentTermsSecure holds details about calls to the PaymentTermsSecure method. + PaymentTermsSecure []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Args is the args argument value. + Args dpp.PaymentTermsArgs + } } - lockPaymentTerms sync.RWMutex + lockPaymentTerms sync.RWMutex + lockPaymentTermsSecure sync.RWMutex } // PaymentTerms calls PaymentTermsFunc. @@ -65,7 +80,8 @@ func (mock *PaymentTermsServiceMock) PaymentTerms(ctx context.Context, args dpp. // PaymentTermsCalls gets all the calls that were made to PaymentTerms. // Check the length with: -// len(mockedPaymentTermsService.PaymentTermsCalls()) +// +// len(mockedPaymentTermsService.PaymentTermsCalls()) func (mock *PaymentTermsServiceMock) PaymentTermsCalls() []struct { Ctx context.Context Args dpp.PaymentTermsArgs @@ -79,3 +95,39 @@ func (mock *PaymentTermsServiceMock) PaymentTermsCalls() []struct { mock.lockPaymentTerms.RUnlock() return calls } + +// PaymentTermsSecure calls PaymentTermsSecureFunc. +func (mock *PaymentTermsServiceMock) PaymentTermsSecure(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) { + if mock.PaymentTermsSecureFunc == nil { + panic("PaymentTermsServiceMock.PaymentTermsSecureFunc: method is nil but PaymentTermsService.PaymentTermsSecure was just called") + } + callInfo := struct { + Ctx context.Context + Args dpp.PaymentTermsArgs + }{ + Ctx: ctx, + Args: args, + } + mock.lockPaymentTermsSecure.Lock() + mock.calls.PaymentTermsSecure = append(mock.calls.PaymentTermsSecure, callInfo) + mock.lockPaymentTermsSecure.Unlock() + return mock.PaymentTermsSecureFunc(ctx, args) +} + +// PaymentTermsSecureCalls gets all the calls that were made to PaymentTermsSecure. +// Check the length with: +// +// len(mockedPaymentTermsService.PaymentTermsSecureCalls()) +func (mock *PaymentTermsServiceMock) PaymentTermsSecureCalls() []struct { + Ctx context.Context + Args dpp.PaymentTermsArgs +} { + var calls []struct { + Ctx context.Context + Args dpp.PaymentTermsArgs + } + mock.lockPaymentTermsSecure.RLock() + calls = mock.calls.PaymentTermsSecure + mock.lockPaymentTermsSecure.RUnlock() + return calls +} diff --git a/payment_terms.go b/payment_terms.go index f52c58c..7dc5e3b 100644 --- a/payment_terms.go +++ b/payment_terms.go @@ -2,6 +2,8 @@ package dpp import ( "context" + + "github.com/libsv/go-bk/envelope" "github.com/libsv/go-dpp/modes/hybridmode" "github.com/libsv/go-dpp/nativetypes" ) @@ -50,7 +52,7 @@ type PaymentTerms struct { Beneficiary *Beneficiary `json:"beneficiary,omitempty"` // PaymentTermsModes TSC payment messages specified by ID (and well defined) messages customer can choose to pay // A key-value map. required field but not if legacy BIP270 outputs are provided - Modes *PaymentTermsModes`json:"modes"` + Modes *PaymentTermsModes `json:"modes"` } // PaymentTermsArgs are request arguments that can be passed to the service. @@ -68,4 +70,5 @@ type PaymentTermsService interface { // PaymentTermsReader will return a new payment request. type PaymentTermsReader interface { PaymentTerms(ctx context.Context, args PaymentTermsArgs) (*PaymentTerms, error) + PaymentTermsSecure(ctx context.Context, args PaymentTermsArgs) (*envelope.JSONEnvelope, error) } From 1b1f7e6fceb14cf75c899573e91105b543969e6f Mon Sep 17 00:00:00 2001 From: Roger Taylor Date: Fri, 16 Sep 2022 12:18:20 -0700 Subject: [PATCH 6/8] Update `go-dpp` for `dpp-proxy` to expect the secure by default `JSONEnvelope` wrapped `PaymentTerms` as the standard. --- mocks/payment_request_service.go | 58 +++----------------------------- mocks/payment_service.go | 23 +++++++------ mocks/payment_writer.go | 23 +++++++------ payment_terms.go | 3 +- 4 files changed, 29 insertions(+), 78 deletions(-) diff --git a/mocks/payment_request_service.go b/mocks/payment_request_service.go index 0c8604d..1a28548 100644 --- a/mocks/payment_request_service.go +++ b/mocks/payment_request_service.go @@ -20,12 +20,9 @@ var _ dpp.PaymentTermsService = &PaymentTermsServiceMock{} // // // make and configure a mocked dpp.PaymentTermsService // mockedPaymentTermsService := &PaymentTermsServiceMock{ -// PaymentTermsFunc: func(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) { +// PaymentTermsFunc: func(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) { // panic("mock out the PaymentTerms method") // }, -// PaymentTermsSecureFunc: func(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) { -// panic("mock out the PaymentTermsSecure method") -// }, // } // // // use mockedPaymentTermsService in code that requires dpp.PaymentTermsService @@ -34,10 +31,7 @@ var _ dpp.PaymentTermsService = &PaymentTermsServiceMock{} // } type PaymentTermsServiceMock struct { // PaymentTermsFunc mocks the PaymentTerms method. - PaymentTermsFunc func(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) - - // PaymentTermsSecureFunc mocks the PaymentTermsSecure method. - PaymentTermsSecureFunc func(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) + PaymentTermsFunc func(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) // calls tracks calls to the methods. calls struct { @@ -48,20 +42,12 @@ type PaymentTermsServiceMock struct { // Args is the args argument value. Args dpp.PaymentTermsArgs } - // PaymentTermsSecure holds details about calls to the PaymentTermsSecure method. - PaymentTermsSecure []struct { - // Ctx is the ctx argument value. - Ctx context.Context - // Args is the args argument value. - Args dpp.PaymentTermsArgs - } } - lockPaymentTerms sync.RWMutex - lockPaymentTermsSecure sync.RWMutex + lockPaymentTerms sync.RWMutex } // PaymentTerms calls PaymentTermsFunc. -func (mock *PaymentTermsServiceMock) PaymentTerms(ctx context.Context, args dpp.PaymentTermsArgs) (*dpp.PaymentTerms, error) { +func (mock *PaymentTermsServiceMock) PaymentTerms(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) { if mock.PaymentTermsFunc == nil { panic("PaymentTermsServiceMock.PaymentTermsFunc: method is nil but PaymentTermsService.PaymentTerms was just called") } @@ -95,39 +81,3 @@ func (mock *PaymentTermsServiceMock) PaymentTermsCalls() []struct { mock.lockPaymentTerms.RUnlock() return calls } - -// PaymentTermsSecure calls PaymentTermsSecureFunc. -func (mock *PaymentTermsServiceMock) PaymentTermsSecure(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) { - if mock.PaymentTermsSecureFunc == nil { - panic("PaymentTermsServiceMock.PaymentTermsSecureFunc: method is nil but PaymentTermsService.PaymentTermsSecure was just called") - } - callInfo := struct { - Ctx context.Context - Args dpp.PaymentTermsArgs - }{ - Ctx: ctx, - Args: args, - } - mock.lockPaymentTermsSecure.Lock() - mock.calls.PaymentTermsSecure = append(mock.calls.PaymentTermsSecure, callInfo) - mock.lockPaymentTermsSecure.Unlock() - return mock.PaymentTermsSecureFunc(ctx, args) -} - -// PaymentTermsSecureCalls gets all the calls that were made to PaymentTermsSecure. -// Check the length with: -// -// len(mockedPaymentTermsService.PaymentTermsSecureCalls()) -func (mock *PaymentTermsServiceMock) PaymentTermsSecureCalls() []struct { - Ctx context.Context - Args dpp.PaymentTermsArgs -} { - var calls []struct { - Ctx context.Context - Args dpp.PaymentTermsArgs - } - mock.lockPaymentTermsSecure.RLock() - calls = mock.calls.PaymentTermsSecure - mock.lockPaymentTermsSecure.RUnlock() - return calls -} diff --git a/mocks/payment_service.go b/mocks/payment_service.go index 4d8c433..1259b22 100644 --- a/mocks/payment_service.go +++ b/mocks/payment_service.go @@ -15,19 +15,19 @@ var _ dpp.PaymentService = &PaymentServiceMock{} // PaymentServiceMock is a mock implementation of dpp.PaymentService. // -// func TestSomethingThatUsesPaymentService(t *testing.T) { +// func TestSomethingThatUsesPaymentService(t *testing.T) { // -// // make and configure a mocked dpp.PaymentService -// mockedPaymentService := &PaymentServiceMock{ -// PaymentCreateFunc: func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) { -// panic("mock out the PaymentCreate method") -// }, -// } +// // make and configure a mocked dpp.PaymentService +// mockedPaymentService := &PaymentServiceMock{ +// PaymentCreateFunc: func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) { +// panic("mock out the PaymentCreate method") +// }, +// } // -// // use mockedPaymentService in code that requires dpp.PaymentService -// // and then make assertions. +// // use mockedPaymentService in code that requires dpp.PaymentService +// // and then make assertions. // -// } +// } type PaymentServiceMock struct { // PaymentCreateFunc mocks the PaymentCreate method. PaymentCreateFunc func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) @@ -69,7 +69,8 @@ func (mock *PaymentServiceMock) PaymentCreate(ctx context.Context, args dpp.Paym // PaymentCreateCalls gets all the calls that were made to PaymentCreate. // Check the length with: -// len(mockedPaymentService.PaymentCreateCalls()) +// +// len(mockedPaymentService.PaymentCreateCalls()) func (mock *PaymentServiceMock) PaymentCreateCalls() []struct { Ctx context.Context Args dpp.PaymentCreateArgs diff --git a/mocks/payment_writer.go b/mocks/payment_writer.go index fcdd485..8e4e196 100644 --- a/mocks/payment_writer.go +++ b/mocks/payment_writer.go @@ -15,19 +15,19 @@ var _ dpp.PaymentWriter = &PaymentWriterMock{} // PaymentWriterMock is a mock implementation of dpp.PaymentWriter. // -// func TestSomethingThatUsesPaymentWriter(t *testing.T) { +// func TestSomethingThatUsesPaymentWriter(t *testing.T) { // -// // make and configure a mocked dpp.PaymentWriter -// mockedPaymentWriter := &PaymentWriterMock{ -// PaymentCreateFunc: func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) { -// panic("mock out the PaymentCreate method") -// }, -// } +// // make and configure a mocked dpp.PaymentWriter +// mockedPaymentWriter := &PaymentWriterMock{ +// PaymentCreateFunc: func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) { +// panic("mock out the PaymentCreate method") +// }, +// } // -// // use mockedPaymentWriter in code that requires dpp.PaymentWriter -// // and then make assertions. +// // use mockedPaymentWriter in code that requires dpp.PaymentWriter +// // and then make assertions. // -// } +// } type PaymentWriterMock struct { // PaymentCreateFunc mocks the PaymentCreate method. PaymentCreateFunc func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) @@ -69,7 +69,8 @@ func (mock *PaymentWriterMock) PaymentCreate(ctx context.Context, args dpp.Payme // PaymentCreateCalls gets all the calls that were made to PaymentCreate. // Check the length with: -// len(mockedPaymentWriter.PaymentCreateCalls()) +// +// len(mockedPaymentWriter.PaymentCreateCalls()) func (mock *PaymentWriterMock) PaymentCreateCalls() []struct { Ctx context.Context Args dpp.PaymentCreateArgs diff --git a/payment_terms.go b/payment_terms.go index 7dc5e3b..79b2c07 100644 --- a/payment_terms.go +++ b/payment_terms.go @@ -69,6 +69,5 @@ type PaymentTermsService interface { // PaymentTermsReader will return a new payment request. type PaymentTermsReader interface { - PaymentTerms(ctx context.Context, args PaymentTermsArgs) (*PaymentTerms, error) - PaymentTermsSecure(ctx context.Context, args PaymentTermsArgs) (*envelope.JSONEnvelope, error) + PaymentTerms(ctx context.Context, args PaymentTermsArgs) (*envelope.JSONEnvelope, error) } From 330cf5077f155762868e4be3350700285d9b940b Mon Sep 17 00:00:00 2001 From: Roger Taylor Date: Fri, 16 Sep 2022 16:57:46 -0700 Subject: [PATCH 7/8] Linting fixes (presumably CI is using latest). --- .golangci.yml | 9 +++++++++ Makefile | 2 +- beneficiary.go | 9 +++++++++ modes/hybridmode/hybrid_payment.go | 10 +++++++++- modes/hybridmode/hybrid_payment_terms.go | 4 ---- nativetypes/native_types.go | 10 +++++++++- 6 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f706d88..2bf2035 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -268,6 +268,11 @@ linters-settings: require-explanation: true # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. require-specific: true + revive: + # This has the worst kind of vague junk warnings about "package-comments" and no useful documentation. + # https://github.com/golangci/golangci-lint/issues/2610 + severity: warning + confidence: 0.8 rowserrcheck: packages: - github.com/jmoiron/sqlx @@ -353,6 +358,10 @@ linters: - goerr113 - testpackage - nlreturn + - deadcode + - scopelint + - varcheck + - structcheck - gci disable-all: false presets: diff --git a/Makefile b/Makefile index e8a3172..856a48b 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ run-linter: install-linter: - @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.35.2 + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.49.0 go-doc-mac: @open http://localhost:6060 && \ diff --git a/beneficiary.go b/beneficiary.go index bcfa9ce..00e22a6 100644 --- a/beneficiary.go +++ b/beneficiary.go @@ -1,3 +1,12 @@ +/* +Package dpp defines some kind of DPP structure. + +These structures are defined in the TSC spec: +https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +This comment is here to qualify as the required make-work for the jobsworth who implemented the +revive "package-comment" linter. +*/ package dpp // Beneficiary to be displayed to the user. diff --git a/modes/hybridmode/hybrid_payment.go b/modes/hybridmode/hybrid_payment.go index baafa6a..2b6edcf 100644 --- a/modes/hybridmode/hybrid_payment.go +++ b/modes/hybridmode/hybrid_payment.go @@ -1,8 +1,16 @@ +/* +Package hybridmode defines the subset of the DPP payment terms structure related to the hybrid mode. + +These structures are defined in the TSC spec: +https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +This comment is here to qualify as the required make-work for the jobsworth who implemented the +revive "package-comment" linter. +*/ package hybridmode import "github.com/libsv/go-bc/spv" - // Payment includes data required for hybridmode payment mode. type Payment struct { // OptionID ID of chosen payment options diff --git a/modes/hybridmode/hybrid_payment_terms.go b/modes/hybridmode/hybrid_payment_terms.go index 17d0855..791c0bc 100644 --- a/modes/hybridmode/hybrid_payment_terms.go +++ b/modes/hybridmode/hybrid_payment_terms.go @@ -4,10 +4,6 @@ import ( "github.com/libsv/go-dpp/nativetypes" ) -// These structures are defined in the TSC spec: -// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol - - // Policies An object containing some policy information like fees or whether Ancestors are // required in the `Payment`. type Policies struct { diff --git a/nativetypes/native_types.go b/nativetypes/native_types.go index d66dc3a..4fb7bb3 100644 --- a/nativetypes/native_types.go +++ b/nativetypes/native_types.go @@ -1,3 +1,12 @@ +/* +Package nativetypes defines some kind of DPP structure. + +These structures are defined in the TSC spec: +https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +This comment is here to qualify as the required make-work for the jobsworth who implemented the +revive "package-comment" linter. +*/ package nativetypes import ( @@ -14,7 +23,6 @@ type NativeOutput struct { Description string `json:"description,omitempty" example:"paymentReference 123456"` } - // NativeInput a way of declaring requirements for the inputs which should be used. It is "native" to distinguish it // from a token input in the hybridmode payment mode. type NativeInput struct { From b674402696530ed764c3bd61b1c3779252012503 Mon Sep 17 00:00:00 2001 From: AustEcon Date: Tue, 11 Oct 2022 14:10:56 +1300 Subject: [PATCH 8/8] Remove `Memo` and `Error` fields of the `PaymentACK` struct because these fields do not feature in the TSC specification --- .golangci.yml | 9 +++++---- payment_ack.go | 9 --------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2bf2035..056983b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -269,10 +269,11 @@ linters-settings: # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. require-specific: true revive: - # This has the worst kind of vague junk warnings about "package-comments" and no useful documentation. - # https://github.com/golangci/golangci-lint/issues/2610 - severity: warning - confidence: 0.8 + rules: + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments + - name: package-comments + severity: warning + disabled: true rowserrcheck: packages: - github.com/jmoiron/sqlx diff --git a/payment_ack.go b/payment_ack.go index 6184ead..ed7d733 100644 --- a/payment_ack.go +++ b/payment_ack.go @@ -16,13 +16,4 @@ type PaymentACK struct { Mode *hybridmode.PaymentACK `json:"mode"` PeerChannel *hybridmode.PeerChannelData `json:"peerChannel"` RedirectURL string `json:"redirectUrl"` - - // Memo may contain information about why there was an error. This field is poorly defined until - // error reporting is more standardised. - Memo string - // A number indicating why the transaction was not accepted. 0 or undefined indicates no error. - // A 1 or any other positive integer indicates an error. The errors are left undefined for now; - // it is recommended only to use “1” and to fill the memo with a textual explanation about why - // the transaction was not accepted until further numbers are defined and standardised. - Error int `json:"error,omitempty"` }