-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #93 from wregulski/wregulski/mapi-arc-replacement
feat: arc api mode implementation
- Loading branch information
Showing
39 changed files
with
1,883 additions
and
860 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Package arc provides the API structures for the ARC service | ||
package arc | ||
|
||
import "github.com/libsv/go-bt/v2" | ||
|
||
// FeePayload is the unmarshalled version of the payload envelope | ||
type FeePayload struct { | ||
Fees *bt.Fee `json:"fees"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package arc | ||
|
||
import "github.com/libsv/go-bt/v2" | ||
|
||
// Policy is the unmarshalled version of the payload envelope | ||
type Policy struct { | ||
MaxScriptSizePolicy uint32 `json:"maxscriptsizepolicy"` | ||
MaxTxSigOpsCount uint32 `json:"maxtxsigopscount"` | ||
MaxTxSizePolicy uint32 `json:"maxtxsizepolicy"` | ||
MiningFee *bt.Fee `json:"miningFee"` | ||
} | ||
|
||
// PolicyQuoteModel is the unmarshalled version of the payload envelope | ||
type PolicyQuoteModel struct { | ||
Policy Policy `json:"policy"` | ||
Timestamp string `json:"timestamp"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package arc | ||
|
||
import "time" | ||
|
||
// QueryTxModel is the unmarshalled version of the payload envelope | ||
type QueryTxModel struct { | ||
BlockHash string `json:"blockHash,omitempty"` | ||
BlockHeight int64 `json:"blockHeight,omitempty"` | ||
// TODO: Specify the type - currently no information on this in the docs | ||
ExtraInfo struct{} `json:"extraInfo,omitempty"` | ||
Timestamp time.Time `json:"timestamp,omitempty"` | ||
TxStatus TxStatus `json:"txStatus,omitempty"` | ||
TxID string `json:"txid,omitempty"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package arc | ||
|
||
import "time" | ||
|
||
// SubmitTxModel is the unmarshalled version of the payload envelope | ||
type SubmitTxModel struct { | ||
BlockHash string `json:"blockHash,omitempty"` | ||
BlockHeight int64 `json:"blockHeight,omitempty"` | ||
ExtraInfo string `json:"extraInfo,omitempty"` | ||
Status int `json:"status,omitempty"` | ||
Timestamp time.Time `json:"timestamp,omitempty"` | ||
Title string `json:"title,omitempty"` | ||
TxStatus TxStatus `json:"txStatus,omitempty"` | ||
TxID string `json:"txid,omitempty"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package arc | ||
|
||
// TxStatus is the status of the transaction | ||
type TxStatus string | ||
|
||
// List of statuses available here: https://github.com/bitcoin-sv/arc | ||
const ( | ||
// Unknown contains value for unknown status | ||
Unknown TxStatus = "UNKNOWN" // 0 | ||
// Queued contains value for queued status | ||
Queued TxStatus = "QUEUED" // 1 | ||
// Received contains value for received status | ||
Received TxStatus = "RECEIVED" // 2 | ||
// Stored contains value for stored status | ||
Stored TxStatus = "STORED" // 3 | ||
// AnnouncedToNetwork contains value for announced to network status | ||
AnnouncedToNetwork TxStatus = "ANNOUNCED_TO_NETWORK" // 4 | ||
// RequestedByNetwork contains value for requested by network status | ||
RequestedByNetwork TxStatus = "REQUESTED_BY_NETWORK" // 5 | ||
// SentToNetwork contains value for sent to network status | ||
SentToNetwork TxStatus = "SENT_TO_NETWORK" // 6 | ||
// AcceptedByNetwork contains value for accepted by network status | ||
AcceptedByNetwork TxStatus = "ACCEPTED_BY_NETWORK" // 7 | ||
// SeenOnNetwork contains value for seen on network status | ||
SeenOnNetwork TxStatus = "SEEN_ON_NETWORK" // 8 | ||
// Mined contains value for mined status | ||
Mined TxStatus = "MINED" // 9 | ||
// Confirmed contains value for confirmed status | ||
Confirmed TxStatus = "CONFIRMED" // 108 | ||
// Rejected contains value for rejected status | ||
Rejected TxStatus = "REJECTED" // 109 | ||
) | ||
|
||
// String returns the string representation of the TxStatus | ||
func (s TxStatus) String() string { | ||
statuses := map[TxStatus]string{ | ||
Unknown: "UNKNOWN", | ||
Queued: "QUEUED", | ||
Received: "RECEIVED", | ||
Stored: "STORED", | ||
AnnouncedToNetwork: "ANNOUNCED_TO_NETWORK", | ||
RequestedByNetwork: "REQUESTED_BY_NETWORK", | ||
SentToNetwork: "SENT_TO_NETWORK", | ||
AcceptedByNetwork: "ACCEPTED_BY_NETWORK", | ||
SeenOnNetwork: "SEEN_ON_NETWORK", | ||
Mined: "MINED", | ||
Confirmed: "CONFIRMED", | ||
Rejected: "REJECTED", | ||
} | ||
|
||
if status, ok := statuses[s]; ok { | ||
return status | ||
} | ||
|
||
return "Can't parse status" | ||
} | ||
|
||
// MapTxStatusToInt maps the TxStatus to an int value | ||
func MapTxStatusToInt(status TxStatus) (int, bool) { | ||
waitForStatusMap := map[TxStatus]int{ | ||
Unknown: 0, | ||
Queued: 1, | ||
Received: 2, | ||
Stored: 3, | ||
AnnouncedToNetwork: 4, | ||
RequestedByNetwork: 5, | ||
SentToNetwork: 6, | ||
AcceptedByNetwork: 7, | ||
SeenOnNetwork: 8, | ||
Mined: 9, | ||
Confirmed: 108, | ||
Rejected: 109, | ||
} | ||
|
||
value, ok := waitForStatusMap[status] | ||
return value, ok | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// Package mapi provides the API structures for the mAPI service | ||
package mapi | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/libsv/go-bt/v2" | ||
) | ||
|
||
const ( | ||
|
||
// FeeTypeData is the key corresponding to the data rate | ||
FeeTypeData = "data" | ||
|
||
// FeeTypeStandard is the key corresponding to the standard rate | ||
FeeTypeStandard = "standard" | ||
|
||
// FeeCategoryMining is the category corresponding to the mining rate | ||
FeeCategoryMining = "mining" | ||
|
||
// FeeCategoryRelay is the category corresponding to the relay rate | ||
FeeCategoryRelay = "relay" | ||
) | ||
|
||
/* | ||
Example feeQuote response from Merchant API: | ||
{ | ||
"payload": "{\"apiVersion\":\"1.4.0\",\"timestamp\":\"2020-10-07T21:13:04.335Z\",\"expiryTime\":\"2020-10-07T21:23:04.335Z\",\"minerId\":\"0211ccfc29e3058b770f3cf3eb34b0b2fd2293057a994d4d275121be4151cdf087\",\"currentHighestBlockHash\":\"000000000000000000edb30c3bbbc8e6a07e522e85522e6a213f7e933e6e2d8d\",\"currentHighestBlockHeight\":655874,\"minerReputation\":null,\"fees\":[{\"feeType\":\"standard\",\"miningFee\":{\"satoshis\":500,\"bytes\":1000},\"relayFee\":{\"satoshis\":250,\"bytes\":1000}},{\"feeType\":\"data\",\"miningFee\":{\"satoshis\":500,\"bytes\":1000},\"relayFee\":{\"satoshis\":250,\"bytes\":1000}}]}", | ||
"signature": "304402206443bea5bdd98a16e23eb61c36b4b998bd68ceb9c84983c7e695e267b21a30440220191571e9b9632c8337d9196723ca20eefa63966ef6360170db0e57a04047453f", | ||
"publicKey": "0211ccfc29e3058b770f3cf3eb34b0b2fd2293057a994d4d275121be4151cdf087", | ||
"encoding": "UTF-8", | ||
"mimetype": "application/json" | ||
} | ||
*/ | ||
|
||
/* | ||
Example FeeQuoteResponse.Payload (unmarshalled): | ||
{ | ||
"apiVersion": "1.4.0", | ||
"timestamp": "2020-10-07T21:13:04.335Z", | ||
"expiryTime": "2020-10-07T21:23:04.335Z", | ||
"minerId": "0211ccfc29e3058b770f3cf3eb34b0b2fd2293057a994d4d275121be4151cdf087", | ||
"currentHighestBlockHash": "000000000000000000edb30c3bbbc8e6a07e522e85522e6a213f7e933e6e2d8d", | ||
"currentHighestBlockHeight": 655874, | ||
"fees": [ | ||
{ | ||
"feeType": "standard", | ||
"miningFee": { | ||
"satoshis": 500, | ||
"bytes": 1000 | ||
}, | ||
"relayFee": { | ||
"satoshis": 250, | ||
"bytes": 1000 | ||
} | ||
}, | ||
{ | ||
"feeType": "data", | ||
"miningFee": { | ||
"satoshis": 500, | ||
"bytes": 1000 | ||
}, | ||
"relayFee": { | ||
"satoshis": 250, | ||
"bytes": 1000 | ||
} | ||
} | ||
] | ||
} | ||
*/ | ||
|
||
// FeePayload is the unmarshalled version of the payload envelope | ||
type FeePayload struct { | ||
FeePayloadFields | ||
Fees []*bt.Fee `json:"fees"` | ||
} | ||
|
||
type ( | ||
|
||
// RawFeePayload is the unmarshalled version of the payload envelope | ||
RawFeePayload struct { | ||
FeePayloadFields | ||
Callbacks []*PolicyCallback `json:"callbacks"` // IP addresses of double-spend notification servers such as mAPI reference implementation | ||
Fees []*FeeObj `json:"fees"` | ||
} | ||
|
||
// FeePayloadFields are the same fields in both payloads | ||
FeePayloadFields struct { | ||
APIVersion string `json:"apiVersion"` | ||
Timestamp string `json:"timestamp"` | ||
ExpirationTime string `json:"expiryTime"` | ||
MinerID string `json:"minerId"` | ||
CurrentHighestBlockHash string `json:"currentHighestBlockHash"` | ||
CurrentHighestBlockHeight uint64 `json:"currentHighestBlockHeight"` | ||
MinerReputation interface{} `json:"minerReputation"` // Not sure what this value is | ||
} | ||
|
||
// FeeUnit displays the amount of Satoshis needed | ||
// for a specific amount of Bytes in a transaction | ||
// see https://github.com/bitcoin-sv-specs/brfc-merchantapi#expanded-payload-1 | ||
FeeUnit struct { | ||
Satoshis int `json:"satoshis"` // Fee in satoshis of the amount of Bytes | ||
Bytes int `json:"bytes"` // Number of bytes that the Fee covers | ||
} | ||
|
||
// FeeObj displays the MiningFee as well as the RelayFee for a specific | ||
// FeeType, for example 'standard' or 'data' | ||
// see https://github.com/bitcoin-sv-specs/brfc-merchantapi#expanded-payload-1 | ||
FeeObj struct { | ||
FeeType string `json:"feeType"` // standard || data | ||
MiningFee FeeUnit `json:"miningFee"` | ||
RelayFee FeeUnit `json:"relayFee"` // Fee for retaining Tx in secondary mempool | ||
} | ||
) | ||
|
||
// CalculateFee will return the fee for the given txBytes | ||
// Type: "FeeTypeData" or "FeeTypeStandard" | ||
// Category: "FeeCategoryMining" or "FeeCategoryRelay" | ||
// | ||
// If no fee is found or fee is 0, returns 1 & error | ||
// | ||
// Spec: https://github.com/bitcoin-sv-specs/brfc-misc/tree/master/feespec#deterministic-transaction-fee-calculation-dtfc | ||
func (f *FeePayload) CalculateFee(feeCategory, feeType string, txBytes uint64) (uint64, error) { | ||
|
||
// Valid feeType? | ||
if !strings.EqualFold(feeType, FeeTypeData) && !strings.EqualFold(feeType, FeeTypeStandard) { | ||
return 0, fmt.Errorf("feeType %s is not recognized", feeType) | ||
} else if !strings.EqualFold(feeCategory, FeeCategoryMining) && !strings.EqualFold(feeCategory, FeeCategoryRelay) { | ||
return 0, fmt.Errorf("feeCategory %s is not recognized", feeCategory) | ||
} | ||
|
||
// Loop all fee types looking for feeType (data or standard) | ||
for _, fee := range f.Fees { | ||
|
||
// Detect the type (data or standard) | ||
if string(fee.FeeType) != feeType { | ||
continue | ||
} | ||
|
||
// Multiply & Divide | ||
var calcFee uint64 | ||
if strings.EqualFold(feeCategory, FeeCategoryMining) { | ||
calcFee = (uint64(fee.MiningFee.Satoshis) * txBytes) / uint64(fee.MiningFee.Bytes) | ||
} else { | ||
calcFee = (uint64(fee.RelayFee.Satoshis) * txBytes) / uint64(fee.RelayFee.Bytes) | ||
} | ||
|
||
// Check for zero | ||
if calcFee != 0 { | ||
return calcFee, nil | ||
} | ||
|
||
// If txBytes is zero this error will occur | ||
return 1, fmt.Errorf("warning: fee calculation was 0") | ||
} | ||
|
||
// No fee type found in the slice of fees | ||
return 1, fmt.Errorf("feeType %s is not found in fees", feeType) | ||
} | ||
|
||
// GetFee will return the fee associated to the type (standard, data) | ||
func (f *FeePayload) GetFee(feeType string) *bt.Fee { | ||
|
||
// Loop the fees for the given type | ||
for index, fee := range f.Fees { | ||
if string(fee.FeeType) == feeType { | ||
return f.Fees[index] | ||
} | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.