Skip to content

Commit 3e4964a

Browse files
committed
Merge branch 'staging-pocket'
2 parents 0817968 + 15aca4d commit 3e4964a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2741
-443
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ servewallet-regtest:
4747
rm -f appfolder.dev/cache/headers-rbtc.bin && rm -rf appfolder.dev/cache/account-*rbtc* && go run -mod=vendor ./cmd/servewallet -regtest
4848
servewallet-prodservers:
4949
go run -mod=vendor ./cmd/servewallet -devservers=false
50+
servewallet-mainnet-prodservers:
51+
go run -mod=vendor ./cmd/servewallet -mainnet -devservers=false
5052
buildweb:
5153
node --version
5254
npm --version

backend/arguments/arguments.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ type Arguments struct {
4848
// Testing stores whether the application is for regtest.
4949
regtest bool
5050

51-
//devservers stores wether the app should connect to the dev servers. The devservers configuration is not persisted when switching back to production.
51+
// devservers stores wether the app should connect to the dev servers.
52+
// This also applies to the Pocket widget environment: if devserver is true, the widget
53+
// will be loaded from the staging environment, otherwise from production.
54+
// The devservers configuration is not persisted when switching back to production.
5255
devservers bool
5356

5457
// gapLimits optionally forces the gap limits used in btc/ltc.

backend/backend.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright 2018 Shift Devices AG
2-
// Copyright 2020 Shift Crypto AG
2+
// Copyright 2022 Shift Crypto AG
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616
package backend
1717

1818
import (
19+
"fmt"
1920
"net/http"
2021
"net/url"
2122
"os"
@@ -79,6 +80,8 @@ var fixedURLWhitelist = []string{
7980
"https://support.moonpay.io/",
8081
"https://help.moonpay.io/",
8182
"https://help.moonpay.com/",
83+
// PocketBitcoin
84+
"https://pocketbitcoin.com/",
8285
// Documentation and other articles.
8386
"https://bitcoincore.org/en/2016/01/26/segwit-benefits/",
8487
"https://en.bitcoin.it/wiki/Bech32_adoption",
@@ -330,6 +333,11 @@ func (backend *Backend) defaultElectrumXServers(code coinpkg.Code) []*config.Ser
330333
return backend.defaultProdServers(code)
331334
}
332335

336+
// DevServers returns the value of the `devservers` flag.
337+
func (backend *Backend) DevServers() bool {
338+
return backend.arguments.DevServers()
339+
}
340+
333341
// Coin returns the coin with the given code or an error if no such coin exists.
334342
func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) {
335343
defer backend.coinsLock.Lock()()
@@ -451,6 +459,11 @@ func (backend *Backend) DevicesRegistered() map[string]device.Interface {
451459
return backend.devices
452460
}
453461

462+
// HTTPClient is a getter method for the HTTPClient instance.
463+
func (backend *Backend) HTTPClient() *http.Client {
464+
return backend.httpClient
465+
}
466+
454467
// Keystore returns the keystore registered at this backend, or nil if no keystore is registered.
455468
func (backend *Backend) Keystore() keystore.Keystore {
456469
defer backend.accountsAndKeystoreLock.RLock()()
@@ -683,3 +696,29 @@ func (backend *Backend) HandleURI(uri string) {
683696
backend.log.Warningf("Unknown URI scheme: %s", uri)
684697
}
685698
}
699+
700+
// GetAccountFromCode takes an account code as input and returns the corresponding accounts.Interface object,
701+
// if found. It also initialize the account before returning it.
702+
func (backend *Backend) GetAccountFromCode(code string) (accounts.Interface, error) {
703+
acctCode := accounts.Code(code)
704+
// TODO: Refactor to make use of a map.
705+
var acct accounts.Interface
706+
for _, a := range backend.Accounts() {
707+
if !a.Config().Active {
708+
continue
709+
}
710+
if a.Config().Code == acctCode {
711+
acct = a
712+
break
713+
}
714+
}
715+
if acct == nil {
716+
return nil, fmt.Errorf("unknown account code %q", acctCode)
717+
}
718+
719+
if err := acct.Initialize(); err != nil {
720+
return nil, err
721+
}
722+
723+
return acct, nil
724+
}

backend/exchanges/exchanges.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2022 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package exchanges
16+
17+
import (
18+
"net/http"
19+
20+
"github.com/digitalbitbox/bitbox-wallet-app/backend/accounts"
21+
"github.com/digitalbitbox/bitbox-wallet-app/util/logging"
22+
)
23+
24+
// ErrorCode are errors that are represented by an error code. This helps the frontend to translate
25+
// error messages.
26+
type ErrorCode string
27+
28+
func (e ErrorCode) Error() string {
29+
return string(e)
30+
}
31+
32+
const (
33+
// ErrAddressNotFound is returned if an address provided for verification is not in the list of unused addresses.
34+
ErrAddressNotFound ErrorCode = "addressNotFound"
35+
36+
// ErrUserAbort is returned if the user aborted the current operation.
37+
ErrUserAbort ErrorCode = "userAbort"
38+
)
39+
40+
// regionCodes is an array containing ISO 3166-1 alpha-2 code of all regions.
41+
// Source: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
42+
var regionCodes = []string{
43+
"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
44+
"BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS",
45+
"BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN",
46+
"CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE",
47+
"EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF",
48+
"GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM",
49+
"HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM",
50+
"JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC",
51+
"LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK",
52+
"ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA",
53+
"NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG",
54+
"PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW",
55+
"SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS",
56+
"ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO",
57+
"TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI",
58+
"VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"}
59+
60+
// ExchangeRegionList contains a list of ExchangeRegion objects.
61+
type ExchangeRegionList struct {
62+
Regions []ExchangeRegion `json:"regions"`
63+
}
64+
65+
// ExchangeRegion contains the ISO 3166-1 alpha-2 code of a specific region and a boolean
66+
//for each exchange, indicating if that exchange is enabled for the region.
67+
type ExchangeRegion struct {
68+
Code string `json:"code"`
69+
IsMoonpayEnabled bool `json:"isMoonpayEnabled"`
70+
IsPocketEnabled bool `json:"isPocketEnabled"`
71+
}
72+
73+
// PaymentMethod type is used for payment options in exchange deals.
74+
type PaymentMethod string
75+
76+
const (
77+
// CardPayment is a payment with credit/debit card.
78+
CardPayment PaymentMethod = "card"
79+
// BankTransferPayment is a payment with bank transfer.
80+
BankTransferPayment PaymentMethod = "bank-transfer"
81+
)
82+
83+
// ExchangeDeal represents a specific purchase option of an exchange.
84+
// - Fee indicates form the percentage that goes to the exchange in a float representation (e.g. 0.01 -> 1%).
85+
// - Payment is the payment method offered in the deal (usually different payment methods bring different fees).
86+
// - IsFast is usually associated with card payments. It is used by the frontend to display the `fast` tag in deals list.
87+
type ExchangeDeal struct {
88+
Fee float32 `json:"fee"`
89+
Payment PaymentMethod `json:"payment"`
90+
IsFast bool `json:"isFast"`
91+
}
92+
93+
// ExchangeDeals list the name of a specific exchange and the list of available deals offered by that exchange.
94+
type ExchangeDeals struct {
95+
ExchangeName string `json:"exchangeName"`
96+
Deals []ExchangeDeal `json:"deals"`
97+
}
98+
99+
// ListExchangesByRegion populates an array of `ExchangeRegion` objects representing the availability
100+
// of the various exchanges in each of them, for the provided account.
101+
// For each region, an exchange is enabled if it supports the account coin and it is active in that region.
102+
// NOTE: if one of the endpoint fails for any reason, the related exchange will be set as available in any
103+
// region by default (for the supported coins).
104+
func ListExchangesByRegion(account accounts.Interface, httpClient *http.Client) ExchangeRegionList {
105+
moonpayRegions, moonpayError := GetMoonpaySupportedRegions(httpClient)
106+
log := logging.Get().WithGroup("exchanges")
107+
if moonpayError != nil {
108+
log.Error(moonpayError)
109+
}
110+
111+
pocketRegions, pocketError := GetPocketSupportedRegions(httpClient)
112+
if pocketError != nil {
113+
log.Error(pocketError)
114+
}
115+
116+
isMoonpaySupported := IsMoonpaySupported(account.Coin().Code())
117+
isPocketSupported := IsPocketSupported(account)
118+
119+
exchangeRegions := ExchangeRegionList{}
120+
for _, code := range regionCodes {
121+
// default behavior is to show the exchange if the supported regions check fails.
122+
moonpayEnabled, pocketEnabled := true, true
123+
if moonpayError == nil {
124+
_, moonpayEnabled = moonpayRegions[code]
125+
}
126+
if pocketError == nil {
127+
_, pocketEnabled = pocketRegions[code]
128+
}
129+
exchangeRegions.Regions = append(exchangeRegions.Regions, ExchangeRegion{
130+
Code: code,
131+
IsMoonpayEnabled: moonpayEnabled && isMoonpaySupported,
132+
IsPocketEnabled: pocketEnabled && isPocketSupported,
133+
})
134+
}
135+
136+
return exchangeRegions
137+
}

backend/exchanges/moonpay.go

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1+
// Copyright 2022 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
115
package exchanges
216

317
import (
418
"fmt"
19+
"net/http"
520
"net/url"
621
"strings"
722

@@ -21,13 +36,19 @@ const (
2136
// - KYC always succeeds; simply click on "submit anyway"
2237
// - need to provide a valid email addr to receive a check code;
2338
// any temp email service like fakermail.com will do
24-
moonpayAPITestPubKey = "pk_test_e9i4oaa4J7eKo8UI3Wm8QLagoskWGjXN"
25-
moonpayAPITestURL = "https://buy-staging.moonpay.com"
39+
moonpayBuyAPITestPubKey = "pk_test_e9i4oaa4J7eKo8UI3Wm8QLagoskWGjXN"
40+
moonpayBuyAPITestURL = "https://buy-staging.moonpay.com"
2641

2742
// moonpayAPILivePubKey is the production API key for real transactions.
2843
// It is ok for it to be public.
29-
moonpayAPILivePubKey = "pk_live_jfhWEt55szMLar8DhQWWiDwteX1mftY"
30-
moonpayAPILiveURL = "https://buy.moonpay.com"
44+
moonpayBuyAPILivePubKey = "pk_live_jfhWEt55szMLar8DhQWWiDwteX1mftY"
45+
moonpayBuyAPILiveURL = "https://buy.moonpay.com"
46+
47+
// moonpayAPILiveURL is the API url for REST calls.
48+
moonpayAPILiveURL = "https://api.moonpay.com/v3"
49+
50+
// MoonpayName is the name of the exchange, it is unique among all the supported exchanges.
51+
MoonpayName = "moonpay"
3152
)
3253

3354
// Here's the list of all supported currencies:
@@ -77,20 +98,65 @@ type BuyMoonpayParams struct {
7798
Lang string // user preferred language in ISO 639-1; falls back to "en"
7899
}
79100

80-
// BuyMoonpay returns info for the frontend to initiate an onramp flow.
81-
func BuyMoonpay(acct accounts.Interface, params BuyMoonpayParams) (BuyMoonpayInfo, error) {
101+
// BuyMoonpayRegion represents informations collected by Moonpay supported countries REST call.
102+
type BuyMoonpayRegion struct {
103+
Alpha2 string `json:"alpha2"` // ISO 3166-1 alpha-2 code of the region (e.g. `CH` for Switzerland)
104+
IsBuyAllowed bool `json:"isBuyAllowed"` // true if Moonpay supports this region
105+
}
106+
107+
// GetMoonpaySupportedRegions query moonpay API and returns a map of regions where buy is allowed.
108+
func GetMoonpaySupportedRegions(httpClient *http.Client) (map[string]BuyMoonpayRegion, error) {
109+
regionsMap := make(map[string]BuyMoonpayRegion)
110+
var regionsList []BuyMoonpayRegion
111+
endpoint := fmt.Sprintf("%s/countries", moonpayAPILiveURL)
112+
113+
err := APIGet(httpClient, endpoint, &regionsList)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
for _, region := range regionsList {
119+
if region.IsBuyAllowed {
120+
regionsMap[region.Alpha2] = region
121+
}
122+
}
123+
124+
return regionsMap, nil
125+
}
126+
127+
// MoonpayDeals returns the purchase conditions (fee and payment methods) offered by Moonpay.
128+
func MoonpayDeals() ExchangeDeals {
129+
return ExchangeDeals{
130+
ExchangeName: MoonpayName,
131+
Deals: []ExchangeDeal{
132+
{
133+
Fee: 0.049, //4.9%
134+
Payment: CardPayment,
135+
IsFast: true,
136+
},
137+
{
138+
Fee: 0.019, //1.9%
139+
Payment: BankTransferPayment,
140+
IsFast: false,
141+
},
142+
},
143+
}
144+
}
145+
146+
// MoonpayInfo returns info for the frontend to initiate an onramp flow.
147+
func MoonpayInfo(acct accounts.Interface, params BuyMoonpayParams) (BuyMoonpayInfo, error) {
82148
if !IsMoonpaySupported(acct.Coin().Code()) {
83149
return BuyMoonpayInfo{}, fmt.Errorf("unsupported cryptocurrency code %q", acct.Coin().Code())
84150
}
85151
ccode, ok := moonpayAPICryptoCode[acct.Coin().Code()]
86152
if !ok {
87153
return BuyMoonpayInfo{}, fmt.Errorf("unknown cryptocurrency code %q", acct.Coin().Code())
88154
}
89-
apiKey := moonpayAPILivePubKey
90-
apiURL := moonpayAPILiveURL
155+
apiKey := moonpayBuyAPILivePubKey
156+
apiURL := moonpayBuyAPILiveURL
91157
if _, isTestnet := coin.TestnetCoins[acct.Coin().Code()]; isTestnet {
92-
apiKey = moonpayAPITestPubKey
93-
apiURL = moonpayAPITestURL
158+
apiKey = moonpayBuyAPITestPubKey
159+
apiURL = moonpayBuyAPITestURL
94160
}
95161
unused := acct.GetUnusedReceiveAddresses()
96162
addr := unused[0].Addresses[0] // TODO: Let them choose sub acct?

0 commit comments

Comments
 (0)