Browse Source

ADD: hodlhodl trading platform integration alpha

settings-bar
Overtorment 5 years ago
parent
commit
1b2bf0d590
  1. 4
      MainBottomTabs.js
  2. 2
      android/app/build.gradle
  3. 4
      class/abstract-wallet.js
  4. 4
      class/hd-segwit-bech32-wallet.js
  5. 132
      class/hodl-hodl-api.js
  6. 20
      class/legacy-wallet.js
  7. 16
      ios/Podfile.lock
  8. 2
      screen/wallets/buyBitcoin.js
  9. 890
      screen/wallets/hodlHodl.js
  10. 24
      screen/wallets/transactions.js
  11. 12
      tests/integration/HDWallet.test.js
  12. 135
      tests/integration/HodlHodl.test.js

4
MainBottomTabs.js

@ -22,6 +22,7 @@ import WalletDetails from './screen/wallets/details';
import WalletExport from './screen/wallets/export';
import WalletXpub from './screen/wallets/xpub';
import BuyBitcoin from './screen/wallets/buyBitcoin';
import HodlHodl from './screen/wallets/hodlHodl';
import Marketplace from './screen/wallets/marketplace';
import ReorderWallets from './screen/wallets/reorderWallets';
import SelectWallet from './screen/wallets/selectWallet';
@ -76,6 +77,9 @@ const WalletsStackNavigator = createStackNavigator(
WalletDetails: {
screen: WalletDetails,
},
HodlHodl: {
screen: HodlHodl,
},
CPFP: {
screen: cpfp,
},

2
android/app/build.gradle

@ -77,7 +77,7 @@ import com.android.build.OutputFile
project.ext.react = [
entryFile: "index.js",
enableHermes: true, // clean and rebuild if changing
enableHermes: false, // clean and rebuild if changing
]
apply from: "../../node_modules/react-native/react.gradle"

4
class/abstract-wallet.js

@ -104,6 +104,10 @@ export class AbstractWallet {
return false;
}
allowHodlHodlTrading() {
return false;
}
weOwnAddress(address) {
throw Error('not implemented');
}

4
class/hd-segwit-bech32-wallet.js

@ -21,6 +21,10 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
return true;
}
allowHodlHodlTrading() {
return true;
}
allowRBF() {
return true;
}

132
class/hodl-hodl-api.js

@ -0,0 +1,132 @@
import Frisbee from 'frisbee';
export class HodlHodlApi {
static PAGINATION_LIMIT = 'limit'; // int
static PAGINATION_OFFSET = 'offset'; // int
static FILTERS_ASSET_CODE = 'asset_code';
static FILTERS_ASSET_CODE_VALUE_BTC = 'BTC';
static FILTERS_ASSET_CODE_VALUE_BTCLN = 'BTCLN';
static FILTERS_SIDE = 'side';
static FILTERS_SIDE_VALUE_BUY = 'buy';
static FILTERS_SIDE_VALUE_SELL = 'sell';
static FILTERS_INCLUDE_GLOBAL = 'include_global'; // bool
static FILTERS_ONLY_WORKING_NOW = 'only_working_now'; // bool
static FILTERS_COUNTRY = 'country'; // code or name (or "Global")
static FILTERS_COUNTRY_VALUE_GLOBAL = 'Global'; // code or name
static FILTERS_CURRENCY_CODE = 'currency_code';
static FILTERS_PAYMENT_METHOD_ID = 'payment_method_id';
static FILTERS_PAYMENT_METHOD_TYPE = 'payment_method_type';
static FILTERS_PAYMENT_METHOD_NAME = 'payment_method_name';
static FILTERS_VOLUME = 'volume';
static FILTERS_PAYMENT_WINDOW_MINUTES_MAX = 'payment_window_minutes_max'; // in minutes
static FILTERS_USER_AVERAGE_PAYMENT_TIME_MINUTES_MAX = 'user_average_payment_time_minutes_max'; // in minutes
static FILTERS_USER_AVERAGE_RELEASE_TIME_MINUTES_MAX = 'user_average_release_time_minutes_max'; // in minutes
static SORT_DIRECTION = 'direction';
static SORT_DIRECTION_VALUE_ASC = 'asc';
static SORT_DIRECTION_VALUE_DESC = 'desc';
static SORT_BY = 'by';
static SORT_BY_VALUE_PRICE = 'price';
static SORT_BY_VALUE_PAYMENT_WINDOW_MINUTES = 'payment_window_minutes';
static SORT_BY_VALUE_USER_AVERAGE_PAYMENT_TIME_MINUTES = 'user_average_payment_time_minutes';
static SORT_BY_VALUE_USER_AVERAGE_RELEASE_TIME_MINUTES = 'user_average_release_time_minutes';
static SORT_BY_VALUE_RATING = 'rating';
constructor(apiKey = false) {
this.baseURI = 'https://hodlhodl.com/';
this.apiKey = apiKey || 'cmO8iLFgx9wrxCe9R7zFtbWpqVqpGuDfXR3FJB0PSGCd7EAh3xgG51vBKgNTAF8fEEpS0loqZ9P1fDZt';
this._api = new Frisbee({ baseURI: this.baseURI });
}
_getHeaders() {
return {
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.apiKey,
},
};
}
async getCountries() {
let response = await this._api.get('/api/v1/countries', this._getHeaders());
let json = response.body;
if (!json || !json.countries || json.status === 'error') {
throw new Error('API failure: ' + JSON.stringify(response));
}
return (this._countries = json.countries);
}
async getMyCountryCode() {
let _api = new Frisbee({ baseURI: 'https://ifconfig.co/' });
let response;
let allowedTries = 6;
while (allowedTries > 0) {
// this API fails a lot, so lets retry several times
response = await _api.get('/country-iso', {
headers: { 'Access-Control-Allow-Origin': '*' },
});
let body = response.body;
if (typeof body === 'string') body = body.replace('\n', '');
if (!body || body.length !== 2) {
allowedTries--;
await (async () => new Promise(resolve => setTimeout(resolve, 3000)))(); // sleep
} else {
return (this._myCountryCode = body);
}
}
throw new Error('API failure after several tries: ' + JSON.stringify(response));
}
async getPaymentMethods(country) {
let response = await this._api.get('/api/v1/payment_methods?filters[country]=' + country, this._getHeaders());
let json = response.body;
if (!json || !json.payment_methods || json.status === 'error') {
throw new Error('API failure: ' + JSON.stringify(response));
}
return (this._payment_methods = json.payment_methods.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)));
}
async getCurrencies() {
let response = await this._api.get('/api/v1/currencies', this._getHeaders());
let json = response.body;
if (!json || !json.currencies || json.status === 'error') {
throw new Error('API failure: ' + JSON.stringify(response));
}
return (this._currencies = json.currencies.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)));
}
async getOffers(pagination = {}, filters = {}, sort = {}) {
let uri = [];
for (let key in sort) {
uri.push('sort[' + key + ']=' + sort[key]);
}
for (let key in filters) {
uri.push('filters[' + key + ']=' + filters[key]);
}
for (let key in pagination) {
uri.push('pagination[' + key + ']=' + pagination[key]);
}
let response = await this._api.get('/api/v1/offers?' + uri.join('&'), this._getHeaders());
let json = response.body;
if (!json || !json.offers || json.status === 'error') {
throw new Error('API failure: ' + JSON.stringify(response));
}
return (this._offers = json.offers);
}
}

20
class/legacy-wallet.js

@ -292,6 +292,26 @@ export class LegacyWallet extends AbstractWallet {
}
}
/**
* Converts script pub key to legacy address if it can. Returns FALSE if it cant.
*
* @param scriptPubKey
* @returns {boolean|string} Either p2pkh address or false
*/
static scriptPubKeyToAddress(scriptPubKey) {
const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex');
let ret;
try {
ret = bitcoin.payments.p2pkh({
output: scriptPubKey2,
network: bitcoin.networks.bitcoin,
}).address;
} catch (_) {
return false;
}
return ret;
}
weOwnAddress(address) {
return this.getAddress() === address || this._address === address;
}

16
ios/Podfile.lock

@ -251,7 +251,7 @@ PODS:
- React
- RemobileReactNativeQrcodeLocalImage (1.0.4):
- React
- RNCAsyncStorage (1.7.1):
- RNCAsyncStorage (1.6.2):
- React
- RNDefaultPreference (1.4.1):
- React
@ -265,13 +265,13 @@ PODS:
- React
- RNQuickAction (0.3.13):
- React
- RNRate (1.1.10):
- RNRate (1.0.1):
- React
- RNReactNativeHapticFeedback (1.9.0):
- React
- RNSecureKeyStore (1.0.0):
- React
- RNSentry (1.3.1):
- RNSentry (1.2.1):
- React
- Sentry (~> 4.4.0)
- RNShare (2.0.0):
@ -340,7 +340,7 @@ DEPENDENCIES:
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNHandoff (from `../node_modules/react-native-handoff`)
- RNQuickAction (from `../node_modules/react-native-quick-actions`)
- RNRate (from `../node_modules/react-native-rate`)
- RNRate (from `../node_modules/react-native-rate/ios`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNSecureKeyStore (from `../node_modules/react-native-secure-key-store/ios`)
- "RNSentry (from `../node_modules/@sentry/react-native`)"
@ -448,7 +448,7 @@ EXTERNAL SOURCES:
RNQuickAction:
:path: "../node_modules/react-native-quick-actions"
RNRate:
:path: "../node_modules/react-native-rate"
:path: "../node_modules/react-native-rate/ios"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
RNSecureKeyStore:
@ -510,17 +510,17 @@ SPEC CHECKSUMS:
ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd
ReactNativePrivacySnapshot: cc295e45dc22810e9ff2c93380d643de20a77015
RemobileReactNativeQrcodeLocalImage: 57aadc12896b148fb5e04bc7c6805f3565f5c3fa
RNCAsyncStorage: 8539fc80a0075fcc9c8e2dff84cd22dc5bf1dacf
RNCAsyncStorage: 5ae4d57458804e99f73d427214442a6b10a53856
RNDefaultPreference: 12d246dd2222e66dadcd76cc1250560663befc3a
RNDeviceInfo: 12faae605ba42a1a5041c3c41a77834bc23f049d
RNFS: 90d1a32d3bc8f75cc7fc3dd2f67506049664346b
RNGestureHandler: 911d3b110a7a233a34c4f800e7188a84b75319c6
RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNRate: d44a8bca6ee08f5d890ecccddaec2810955ffbb3
RNRate: 29be49c24b314c4e8ec09d848c3965f61cb0be47
RNReactNativeHapticFeedback: 2566b468cc8d0e7bb2f84b23adc0f4614594d071
RNSecureKeyStore: f1ad870e53806453039f650720d2845c678d89c8
RNSentry: 6458ba85aa3f8ae291abed4f72abbd7080839c71
RNSentry: 9b1d983b2d5d1c215ba6490348fd2a4cc23a8a9d
RNShare: 8b171d4b43c1d886917fdd303bf7a4b87167b05c
RNSVG: 8ba35cbeb385a52fd960fd28db9d7d18b4c2974f
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4

2
screen/wallets/buyBitcoin.js

@ -27,7 +27,7 @@ export default class BuyBitcoin extends Component {
}
async componentDidMount() {
console.log('buyBitcoin/details - componentDidMount');
console.log('buyBitcoin - componentDidMount');
/** @type {AbstractWallet} */
let wallet;

890
screen/wallets/hodlHodl.js

File diff suppressed because one or more lines are too long

24
screen/wallets/transactions.js

@ -222,6 +222,7 @@ export default class WalletTransactions extends Component {
*/}
{this.renderMarketplaceButton()}
{this.state.wallet.type === LightningCustodianWallet.type && Platform.OS === 'ios' && this.renderLappBrowserButton()}
{this.state.wallet.allowHodlHodlTrading() && this.renderHodlHodlButton()}
</View>
<Text
style={{
@ -375,6 +376,29 @@ export default class WalletTransactions extends Component {
);
};
renderHodlHodlButton = () => {
return (
<TouchableOpacity
onPress={() => {
this.props.navigation.navigate('HodlHodl', { fromWallet: this.state.wallet });
}}
style={{
marginLeft: 5,
backgroundColor: '#f2f2f2',
borderRadius: 9,
minHeight: 49,
flex: 1,
paddingHorizontal: 8,
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
}}
>
<Text style={{ color: '#062453', fontSize: 18 }}>local trader</Text>
</TouchableOpacity>
);
};
onWalletSelect = async wallet => {
if (wallet) {
NavigationService.navigate('WalletTransactions', {

12
tests/integration/HDWallet.test.js

@ -1,5 +1,12 @@
/* global it, jasmine, afterAll, beforeAll */
import { SegwitP2SHWallet, SegwitBech32Wallet, HDSegwitP2SHWallet, HDLegacyBreadwalletWallet, HDLegacyP2PKHWallet } from '../../class';
import {
SegwitP2SHWallet,
SegwitBech32Wallet,
HDSegwitP2SHWallet,
HDLegacyBreadwalletWallet,
HDLegacyP2PKHWallet,
LegacyWallet,
} from '../../class';
import { BitcoinUnit } from '../../models/bitcoinUnits';
const bitcoin = require('bitcoinjs-lib');
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
@ -37,6 +44,9 @@ it('can convert witness to address', () => {
address = SegwitBech32Wallet.scriptPubKeyToAddress('00144d757460da5fcaf84cc22f3847faaa1078e84f6a');
assert.strictEqual(address, 'bc1qf46hgcx6tl90snxz9uuy0742zpuwsnm27ysdh7');
address = LegacyWallet.scriptPubKeyToAddress('76a914d0b77eb1502c81c4093da9aa6eccfdf560cdd6b288ac');
assert.strictEqual(address, '1L2bNMGRQQLT2AVUek4K9L7sn3SSMioMgE');
});
it('can create a Segwit HD (BIP49)', async function() {

135
tests/integration/HodlHodl.test.js

@ -0,0 +1,135 @@
/* global it, jasmine, describe */
import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet } from '../../class';
import { HodlHodlApi } from '../../class/hodl-hodl-api';
const bitcoin = require('bitcoinjs-lib');
const assert = require('assert');
it('can create escrow address', () => {
const keyPairServer = bitcoin.ECPair.fromPrivateKey(
Buffer.from('9a8cfd0e33a37c90a46d358c84ca3d8dd089ed35409a6eb1973148c0df492288', 'hex'),
);
const keyPairSeller = bitcoin.ECPair.fromPrivateKey(
Buffer.from('ab4163f517bfac01d7acd3a1e398bfb28b53ebd162cb1dd767cc63ae8069ef37', 'hex'),
);
const keyPairBuyer = bitcoin.ECPair.fromPrivateKey(
Buffer.from('b4ab9ed098b6d4b308deaefce5079f4203c43cfb51b699dd35dcc0f1ae5906fd', 'hex'),
);
const pubkeys = [
keyPairServer.publicKey, // '03141024b18929bfec5b567c12b1693d4ae02783873e2e3aa444f0d6950cb97dee', // server
keyPairSeller.publicKey, // '0208137b6cb23cef02c0529948a2ed12fbeed0813cce555de073319f56e215ee1b', // seller
keyPairBuyer.publicKey, // '035ed5825258d4f1685df804f21296b9957cd319cf5949ace92fa5767eb7a946f2', // buyer
].map(hex => Buffer.from(hex, 'hex'));
const p2shP2wshP2ms = bitcoin.payments.p2sh({
redeem: bitcoin.payments.p2wsh({
redeem: bitcoin.payments.p2ms({ m: 2, pubkeys }),
}),
});
const address = p2shP2wshP2ms.address;
// console.warn(p2sh_p2wsh_p2ms);
assert.strictEqual(address, '391ygT71qeF7vbYjxsUZPzH6oDc7Rv4vTs');
let signedByServerReleaseTransaction =
'01000000000101356493a6b93bf17e66d7ec12f1a54e279da17f669f41bf11405a6f2617e1022501000000232200208ec72df31adaa132e40a5f5033589c0e18b67a64cdc65e9c75027fe1efd10f4cffffffff02227e010000000000160014b1c61a73a529c315a1f2b87df12c7948d86ba10c26020000000000001976a914d0b77eb1502c81c4093da9aa6eccfdf560cdd6b288ac040047304402205a447563db8e74177a1fbcdcfe7b7b22556c39d68c17ffe0a4a02609d78c83130220772fbf3261b6031a915eca7e441092df3fe6e4c7d4f389c4921c1f18661c20f401460000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000069522103141024b18929bfec5b567c12b1693d4ae02783873e2e3aa444f0d6950cb97dee210208137b6cb23cef02c0529948a2ed12fbeed0813cce555de073319f56e215ee1b21035ed5825258d4f1685df804f21296b9957cd319cf5949ace92fa5767eb7a946f253ae00000000';
let txDecoded = bitcoin.Transaction.fromHex(signedByServerReleaseTransaction);
// console.warn(txDecoded.ins[0].witness);
// we always expect only one input:
const psbt = new bitcoin.Psbt().addInput({
hash: txDecoded.ins[0].hash,
index: txDecoded.ins[0].index,
witnessUtxo: {
script: p2shP2wshP2ms.redeem.output,
value: 100000,
},
// redeemScript,
witnessScript: p2shP2wshP2ms.redeem.redeem.output,
});
for (let out of txDecoded.outs) {
let scripthex = out.script.toString('hex');
let address =
LegacyWallet.scriptPubKeyToAddress(scripthex) ||
SegwitP2SHWallet.scriptPubKeyToAddress(scripthex) ||
SegwitBech32Wallet.scriptPubKeyToAddress(scripthex);
psbt.addOutput({
address,
value: out.value,
});
}
// psbt.signInput(0, keyPairServer);
psbt.signInput(0, keyPairSeller);
// console.warn('signature = ', psbt.data.inputs[0].partialSig[0].signature.toString('hex'));
// let tx = psbt.finalizeAllInputs().extractTransaction();
// console.log(tx.toHex());
});
describe('HodlHodl API', function() {
it('can fetch countries & and own country code', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000;
let Hodl = new HodlHodlApi();
const countries = await Hodl.getCountries();
assert.ok(countries[0]);
assert.ok(countries[0].code);
assert.ok(countries[0].name);
assert.ok(countries[0].native_name);
assert.ok(countries[0].currency_code);
assert.ok(countries[0].currency_name);
let countryCode = await Hodl.getMyCountryCode();
assert.strictEqual(countryCode.length, 2);
});
it('can get offers', async () => {
let Hodl = new HodlHodlApi();
const offers = await Hodl.getOffers(
{
[HodlHodlApi.PAGINATION_LIMIT]: 10,
},
{
[HodlHodlApi.FILTERS_COUNTRY]: HodlHodlApi.FILTERS_COUNTRY_VALUE_GLOBAL,
[HodlHodlApi.FILTERS_SIDE]: HodlHodlApi.FILTERS_SIDE_VALUE_SELL,
[HodlHodlApi.FILTERS_ASSET_CODE]: HodlHodlApi.FILTERS_ASSET_CODE_VALUE_BTC,
[HodlHodlApi.FILTERS_INCLUDE_GLOBAL]: true,
},
{
[HodlHodlApi.SORT_BY]: HodlHodlApi.SORT_BY_VALUE_PRICE,
[HodlHodlApi.SORT_DIRECTION]: HodlHodlApi.SORT_DIRECTION_VALUE_ASC,
},
);
assert.ok(offers[0]);
assert.ok(offers[0].asset_code === 'BTC');
assert.ok(offers[0].country_code);
assert.ok(offers[0].side === HodlHodlApi.FILTERS_SIDE_VALUE_SELL);
assert.ok(offers[0].title || offers[0].description);
assert.ok(offers[0].price);
assert.ok(offers[0].payment_method_instructions);
assert.ok(offers[0].trader);
});
it('can get payment methods', async () => {
let Hodl = new HodlHodlApi();
const methods = await Hodl.getPaymentMethods(HodlHodlApi.FILTERS_COUNTRY_VALUE_GLOBAL);
assert.ok(methods[0]);
assert.ok(methods[0].id);
assert.ok(methods[0].type);
assert.ok(methods[0].name);
});
it('cat get currencies', async () => {
let Hodl = new HodlHodlApi();
const currencies = await Hodl.getCurrencies();
assert.ok(currencies[0]);
assert.ok(currencies[0].code);
assert.ok(currencies[0].name);
assert.ok(currencies[0].type);
});
});
Loading…
Cancel
Save