├── .watchmanconfig
├── .gitattributes
├── android
├── local.properties
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ └── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ ├── assets
│ │ │ │ └── fonts
│ │ │ │ │ ├── Entypo.ttf
│ │ │ │ │ ├── Zocial.ttf
│ │ │ │ │ ├── Feather.ttf
│ │ │ │ │ ├── Fontisto.ttf
│ │ │ │ │ ├── Ionicons.ttf
│ │ │ │ │ ├── Octicons.ttf
│ │ │ │ │ ├── AntDesign.ttf
│ │ │ │ │ ├── EvilIcons.ttf
│ │ │ │ │ ├── FontAwesome.ttf
│ │ │ │ │ ├── Foundation.ttf
│ │ │ │ │ ├── MaterialIcons.ttf
│ │ │ │ │ ├── SimpleLineIcons.ttf
│ │ │ │ │ ├── FontAwesome5_Solid.ttf
│ │ │ │ │ ├── FontAwesome5_Brands.ttf
│ │ │ │ │ ├── FontAwesome5_Regular.ttf
│ │ │ │ │ └── MaterialCommunityIcons.ttf
│ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── partnr
│ │ │ │ │ ├── MainActivity.java
│ │ │ │ │ └── MainApplication.java
│ │ │ └── AndroidManifest.xml
│ │ └── debug
│ │ │ └── AndroidManifest.xml
│ ├── .project
│ ├── build_defs.bzl
│ ├── proguard-rules.pro
│ └── BUCK
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── keystores
│ ├── debug.keystore.properties
│ └── BUCK
├── .project
├── gradle.properties
├── build.gradle
├── settings.gradle
└── gradlew.bat
├── app.json
├── src
├── common
│ ├── constants
│ │ ├── recents.js
│ │ ├── wallet.js
│ │ ├── storage.js
│ │ ├── index.js
│ │ └── url.js
│ ├── styles
│ │ ├── index.js
│ │ ├── measures.js
│ │ └── colors.js
│ ├── utils
│ │ ├── index.js
│ │ ├── image.js
│ │ ├── __tests__
│ │ │ ├── image.js
│ │ │ └── shims.js
│ │ ├── transaction.js
│ │ ├── shims.js
│ │ └── wallet.js
│ ├── stores
│ │ ├── index.js
│ │ ├── recents.js
│ │ ├── prices.js
│ │ ├── wallets.js
│ │ ├── wallet.js
│ │ └── __tests__
│ │ │ ├── recents.js
│ │ │ ├── wallets.js
│ │ │ └── prices.js
│ ├── services
│ │ ├── index.js
│ │ ├── api.js
│ │ ├── storage.js
│ │ ├── recents.js
│ │ ├── wallets.js
│ │ ├── transactions.js
│ │ └── __tests__
│ │ │ ├── api.js
│ │ │ └── transaction.js
│ └── actions
│ │ ├── index.js
│ │ ├── prices.js
│ │ ├── __tests__
│ │ ├── price.js
│ │ ├── general.js
│ │ ├── recents.js
│ │ ├── transactions.js
│ │ └── wallets.js
│ │ ├── recents.js
│ │ ├── transactions.js
│ │ ├── general.js
│ │ └── wallets.js
├── assets
│ └── ethereum-logo.png
├── setupTests.js
├── components
│ ├── widgets
│ │ ├── index.js
│ │ ├── TextBullet
│ │ │ ├── test.js
│ │ │ └── index.js
│ │ ├── HeaderIcon
│ │ │ ├── test.js
│ │ │ └── index.js
│ │ ├── InputWithIcon
│ │ │ ├── test.js
│ │ │ └── index.js
│ │ ├── Button
│ │ │ ├── test.js
│ │ │ └── index.js
│ │ ├── TabView
│ │ │ ├── index.js
│ │ │ ├── TabBarIcon.js
│ │ │ └── TabBar.js
│ │ ├── Icon
│ │ │ ├── test.js
│ │ │ └── index.js
│ │ ├── Calculator
│ │ │ ├── index.js
│ │ │ └── Panel.js
│ │ ├── NumberGrid
│ │ │ └── index.js
│ │ └── Camera
│ │ │ └── index.js
│ └── views
│ │ ├── SelectDestination
│ │ ├── NoRecents.js
│ │ ├── index.js
│ │ └── Recents.js
│ │ ├── Settings
│ │ ├── ListItem.js
│ │ └── index.js
│ │ ├── ChangeCurrency
│ │ ├── ListItem.js
│ │ └── index.js
│ │ ├── WalletSettings
│ │ ├── ListItem.js
│ │ └── index.js
│ │ ├── WalletExtract
│ │ ├── NoTransactions.js
│ │ ├── Balance.js
│ │ ├── index.js
│ │ └── TransactionCard.js
│ │ ├── index.js
│ │ ├── SendCoins
│ │ └── index.js
│ │ ├── WalletDetails
│ │ └── index.js
│ │ ├── ConfirmTransaction
│ │ ├── ErrorMessage.js
│ │ ├── SuccessMessage.js
│ │ └── index.js
│ │ ├── ShowPrivateKey
│ │ └── index.js
│ │ ├── LoadWallet
│ │ └── index.js
│ │ ├── WalletsOverview
│ │ ├── NoWallets.js
│ │ ├── TotalBalance.js
│ │ ├── index.js
│ │ └── WalletCard.js
│ │ ├── NewWallet
│ │ └── index.js
│ │ ├── CreateWallet
│ │ └── index.js
│ │ ├── ConfirmMnemonics
│ │ ├── index.js
│ │ └── ConfirmBox.js
│ │ ├── ReceiveCoins
│ │ └── index.js
│ │ ├── CreateMnemonics
│ │ └── index.js
│ │ ├── NewWalletName
│ │ └── index.js
│ │ ├── LoadPrivateKey
│ │ └── index.js
│ │ └── LoadMnemonics
│ │ └── index.js
├── index.js
├── folderStructure.txt
└── Router.js
├── ios
├── Partnr
│ ├── Images.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── AppDelegate.h
│ ├── main.m
│ ├── AppDelegate.m
│ ├── Info.plist
│ └── Base.lproj
│ │ └── LaunchScreen.xib
├── PartnrTests
│ ├── Info.plist
│ └── PartnrTests.m
├── Partnr-tvOSTests
│ └── Info.plist
└── Partnr-tvOS
│ └── Info.plist
├── tsconfig.json
├── .buckconfig
├── migrations
├── 1_initial_migration.js
└── 2_deploy_contracts.js
├── rn-cli.config.js
├── __mocks__
├── react-native-snackbar.js
└── react-native-camera.js
├── config
└── jest.config.js
├── metro.config.js
├── index.js
├── babel.config.js
├── contracts
├── Migrations.sol
├── MultiSigWalletFactory.sol
├── Factory.sol
├── Escrow.sol
└── Votes.sol
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── .gitignore
├── .circleci
└── config.yml
├── .flowconfig
├── package.json
└── README.md
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 |
--------------------------------------------------------------------------------
/android/local.properties:
--------------------------------------------------------------------------------
1 | sdk.dir = /Users/carlos/Library/Android/sdk
2 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Partnr",
3 | "displayName": "Partnr"
4 | }
--------------------------------------------------------------------------------
/src/common/constants/recents.js:
--------------------------------------------------------------------------------
1 | export const STORAGE_KEY = '@Storage/Recents';
--------------------------------------------------------------------------------
/src/common/constants/wallet.js:
--------------------------------------------------------------------------------
1 | export const STORAGE_KEY = '@Storage/Wallet';
--------------------------------------------------------------------------------
/src/assets/ethereum-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/src/assets/ethereum-logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Partnr
3 |
4 |
--------------------------------------------------------------------------------
/ios/Partnr/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "allowJs": true
5 | }
6 | }
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Entypo.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Entypo.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Zocial.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Zocial.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Feather.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Feather.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Fontisto.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Fontisto.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Ionicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Octicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Octicons.ttf
--------------------------------------------------------------------------------
/src/common/styles/index.js:
--------------------------------------------------------------------------------
1 | import * as colors from './colors';
2 | import * as measures from './measures';
3 |
4 | export { colors, measures };
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/AntDesign.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/AntDesign.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/EvilIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/EvilIcons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/FontAwesome.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/FontAwesome.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Foundation.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Foundation.ttf
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 | configure({ adapter: new Adapter() });
--------------------------------------------------------------------------------
/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/MaterialIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/SimpleLineIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/SimpleLineIcons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/keystores/debug.keystore.properties:
--------------------------------------------------------------------------------
1 | key.store=debug.keystore
2 | key.alias=androiddebugkey
3 | key.store.password=android
4 | key.alias.password=android
5 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/migrations/1_initial_migration.js:
--------------------------------------------------------------------------------
1 | const Migrations = artifacts.require("Migrations");
2 |
3 | module.exports = function(deployer) {
4 | deployer.deploy(Migrations);
5 | };
6 |
--------------------------------------------------------------------------------
/src/common/constants/storage.js:
--------------------------------------------------------------------------------
1 | export const CONFIG = {
2 | sharedPreferencesName: '@BitWalletPrefs-712638173',
3 | keychainService: '@BitWalletKeychain-513961259'
4 | };
--------------------------------------------------------------------------------
/src/common/utils/index.js:
--------------------------------------------------------------------------------
1 | import * as Image from './image';
2 | import * as Transaction from './transaction';
3 | import * as Wallet from './wallet';
4 |
5 | export { Image, Transaction, Wallet };
--------------------------------------------------------------------------------
/android/keystores/BUCK:
--------------------------------------------------------------------------------
1 | keystore(
2 | name = "debug",
3 | properties = "debug.keystore.properties",
4 | store = "debug.keystore",
5 | visibility = [
6 | "PUBLIC",
7 | ],
8 | )
9 |
--------------------------------------------------------------------------------
/rn-cli.config.js:
--------------------------------------------------------------------------------
1 | const blacklist = require('metro/src/blacklist')
2 | module.exports = {
3 | getBlacklistRE () {
4 | return blacklist([/react-native\/local-cli\/core\/__fixtures__.*/])
5 | },
6 | }
--------------------------------------------------------------------------------
/migrations/2_deploy_contracts.js:
--------------------------------------------------------------------------------
1 | const MultisigWalletFactory = artifacts.require('MultiSigWalletWithDailyLimitFactory.sol')
2 |
3 | module.exports = function (deployer) {
4 | deployer.deploy(MultisigWalletFactory)
5 | }
6 |
--------------------------------------------------------------------------------
/src/common/stores/index.js:
--------------------------------------------------------------------------------
1 | import prices from './prices';
2 | import recents from './recents';
3 | import wallet from './wallet';
4 | import wallets from './wallets';
5 |
6 | export { prices, recents, wallet, wallets };
7 |
--------------------------------------------------------------------------------
/src/common/constants/index.js:
--------------------------------------------------------------------------------
1 | import * as Recents from './recents';
2 | import * as Storage from './storage';
3 | import * as Url from './url';
4 | import * as Wallet from './wallet';
5 |
6 | export { Recents, Storage, Url, Wallet };
7 |
--------------------------------------------------------------------------------
/src/common/constants/url.js:
--------------------------------------------------------------------------------
1 | export const CRYPTO_COMPARE = 'https://min-api.cryptocompare.com';
2 |
3 | export const ETHERSCAN = (process.env.NODE_ENV === 'production') ? 'https://api.etherscan.io/api' : 'https://rinkeby.etherscan.io/api';
--------------------------------------------------------------------------------
/__mocks__/react-native-snackbar.js:
--------------------------------------------------------------------------------
1 | export const LENGTH_LONG = 'LENGTH_LONG';
2 | export const LENGTH_INDEFINITE = 'LENGTH_INDEFINITE';
3 | export const LENGTH_SHORT = 'LENGTH_SHORT';
4 |
5 |
6 | export function show({ title, duration }) {}
--------------------------------------------------------------------------------
/src/common/utils/image.js:
--------------------------------------------------------------------------------
1 | import Identicon from 'identicon.js';
2 |
3 | export function generateAvatar(hash) {
4 | const options = { size: 500 };
5 | const avatar = new Identicon(hash, options).toString();
6 | return `data:image/png;base64,${avatar}`;
7 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/config/jest.config.js:
--------------------------------------------------------------------------------
1 | require("react-native-mock-render/mock");
2 | const enzyme = require('enzyme');
3 | const Adapter = require('enzyme-adapter-react-16');
4 | const { JSDOM } = require("jsdom");
5 |
6 | global = new JSDOM("");
7 | enzyme.configure({ adapter: new Adapter() });
--------------------------------------------------------------------------------
/src/common/services/index.js:
--------------------------------------------------------------------------------
1 | import * as Api from './api';
2 | import * as Recents from './recents';
3 | import * as Storage from './storage';
4 | import * as Transactions from './transactions';
5 | import * as Wallets from './wallets';
6 |
7 | export { Api, Recents, Storage, Transactions, Wallets };
--------------------------------------------------------------------------------
/src/common/actions/index.js:
--------------------------------------------------------------------------------
1 | import * as General from './general';
2 | import * as Prices from './prices';
3 | import * as Recents from './recents';
4 | import * as Transactions from './transactions';
5 | import * as Wallets from './wallets';
6 |
7 | export { General, Prices, Recents, Transactions, Wallets };
--------------------------------------------------------------------------------
/src/components/widgets/index.js:
--------------------------------------------------------------------------------
1 | export * from './Button';
2 | export * from './Calculator';
3 | export * from './Camera';
4 | export * from './HeaderIcon';
5 | export * from './Icon';
6 | export * from './InputWithIcon';
7 | export * from './NumberGrid';
8 | export * from './TabView';
9 | export * from './TextBullet';
--------------------------------------------------------------------------------
/__mocks__/react-native-camera.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 |
4 | export class RNCamera extends React.Component {
5 |
6 | static Constants = {
7 | BarCodeType: 'BarCodeType'
8 | };
9 |
10 | render() {
11 | return (
12 |
13 | );
14 | }
15 | }
--------------------------------------------------------------------------------
/src/common/utils/__tests__/image.js:
--------------------------------------------------------------------------------
1 | import * as ImageUtils from '../image';
2 |
3 | describe('Image utils', () => {
4 |
5 | it('`generateAvatar` function should return a base64 encoded SVG', () => {
6 | const result = ImageUtils.generateAvatar('123456789012345');
7 | expect(typeof result).toEqual('string');
8 | });
9 | });
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Metro configuration for React Native
3 | * https://github.com/facebook/react-native
4 | *
5 | * @format
6 | */
7 |
8 | module.exports = {
9 | transformer: {
10 | getTransformOptions: async () => ({
11 | transform: {
12 | experimentalImportSupport: false,
13 | inlineRequires: false,
14 | },
15 | }),
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/common/styles/measures.js:
--------------------------------------------------------------------------------
1 | export const defaultMargin = 8;
2 | export const defaultPadding = 8;
3 |
4 | export const fontSizeMedium = 18;
5 | export const fontSizeSmall = fontSizeMedium * 0.6;
6 | export const fontSizeLarge = fontSizeMedium * 1.5;
7 |
8 | export const iconSizeMedium = 24;
9 | export const iconSizeSmall = iconSizeMedium * 0.6;
10 | export const iconSizeLarge = iconSizeMedium * 1.5;
--------------------------------------------------------------------------------
/src/common/services/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { Url } from '@common/constants';
3 |
4 | export function getPrice() {
5 | return axios.get(`${Url.CRYPTO_COMPARE}/data/price?fsym=ETH&tsyms=USD,EUR,JMD`);
6 | }
7 |
8 | export function getHistory(address) {
9 | return axios.get(`${Url.ETHERSCAN}?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&sort=asc`);
10 | }
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/common/utils/__tests__/shims.js:
--------------------------------------------------------------------------------
1 | import '../shims';
2 |
3 | describe('Shims', () => {
4 |
5 | it('should add the method `slice` to `Uint8Array` prototype', () => {
6 | expect(Uint8Array.prototype.slice).toBeInstanceOf(Function);
7 | });
8 |
9 | it('should add the method `equals` to `Array` prototype', () => {
10 | expect(Array.prototype.equals).toBeInstanceOf(Function);
11 | });
12 | });
--------------------------------------------------------------------------------
/src/common/actions/prices.js:
--------------------------------------------------------------------------------
1 | import { Api as ApiService } from '@common/services';
2 | import { prices as PricesStore } from '@common/stores';
3 |
4 | export async function getPrice() {
5 | PricesStore.isLoading(true);
6 | const { data } = await ApiService.getPrice();
7 | PricesStore.setUSDRate(data.USD);
8 | PricesStore.setEURRate(data.EUR);
9 | PricesStore.setJMDRate(data.JMD);
10 | PricesStore.isLoading(false);
11 | }
--------------------------------------------------------------------------------
/src/components/widgets/TextBullet/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from 'react-native';
3 | import { shallow } from 'enzyme';
4 | import { TextBullet } from './index';
5 |
6 | describe('', () => {
7 |
8 | it('should have a label `label`', () => {
9 | const wrapper = shallow(label);
10 | expect(wrapper.find(Text).children().text()).toEqual('label');
11 | });
12 | });
--------------------------------------------------------------------------------
/android/app/src/main/java/com/partnr/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.partnr;
2 |
3 | import com.facebook.react.ReactActivity;
4 |
5 | public class MainActivity extends ReactActivity {
6 |
7 | /**
8 | * Returns the name of the main component registered from JavaScript.
9 | * This is used to schedule rendering of the component.
10 | */
11 | @Override
12 | protected String getMainComponentName() {
13 | return "Partnr";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { AppRegistry, YellowBox } from 'react-native';
2 | import App from './src';
3 | import './src/common/utils/shims';
4 | import { name as appName } from './app.json';
5 |
6 | YellowBox.ignoreWarnings([
7 | 'Warning: componentWillMount',
8 | 'Warning: componentWillReceiveProps',
9 | 'Module RCTImageLoader',
10 | 'Class RCTCxxModule was not exported',
11 | 'Remote debugger'
12 | ]);
13 |
14 | AppRegistry.registerComponent(appName, () => App);
15 |
--------------------------------------------------------------------------------
/ios/Partnr/AppDelegate.h:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | #import
9 | #import
10 |
11 | @interface AppDelegate : UIResponder
12 |
13 | @property (nonatomic, strong) UIWindow *window;
14 |
15 | @end
16 |
--------------------------------------------------------------------------------
/ios/Partnr/main.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | #import
9 |
10 | #import "AppDelegate.h"
11 |
12 | int main(int argc, char * argv[]) {
13 | @autoreleasepool {
14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/widgets/HeaderIcon/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { HeaderIcon } from './index';
4 |
5 | describe('', () => {
6 |
7 | it('should call the `onPress` function when clicked', () => {
8 | const onPress = jest.fn();
9 | const wrapper = shallow();
10 | wrapper.simulate('press');
11 | expect(onPress).toBeCalled();
12 | });
13 | });
--------------------------------------------------------------------------------
/android/app/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | app
4 | Project app created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.buildship.core.gradleprojectbuilder
10 |
11 |
12 |
13 |
14 |
15 | org.eclipse.buildship.core.gradleprojectnature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/android/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | Partnr
4 | Project android created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.buildship.core.gradleprojectbuilder
10 |
11 |
12 |
13 |
14 |
15 | org.eclipse.buildship.core.gradleprojectnature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/common/services/storage.js:
--------------------------------------------------------------------------------
1 | import SensitiveInfoStorage from 'react-native-sensitive-info';
2 | import { Storage } from '@common/constants';
3 |
4 | export function getItem(key) {
5 | return SensitiveInfoStorage.getItem(key, Storage.CONFIG).then(item => item || '');
6 | }
7 |
8 | export function setItem(key, value) {
9 | return SensitiveInfoStorage.setItem(key, value || '', Storage.CONFIG);
10 | }
11 |
12 | export function deleteItem(key) {
13 | return SensitiveInfoStorage.deleteItem(key, Storage.CONFIG);
14 | }
--------------------------------------------------------------------------------
/src/common/actions/__tests__/price.js:
--------------------------------------------------------------------------------
1 | import * as Action from '../prices';
2 | import { prices as PricesStore } from '@common/stores';
3 |
4 | describe('PriceActions', () => {
5 |
6 | it('should add the prices to the store', async function() {
7 | try {
8 | await Action.getPrice();
9 | expect(PricesStore.usd).toBeGreaterThan(0);
10 | expect(PricesStore.eur).toBeGreaterThan(0);
11 | expect(PricesStore.jmd).toBeGreaterThan(0);
12 | } catch (e) {
13 | fail(e);
14 | }
15 | });
16 | });
--------------------------------------------------------------------------------
/src/common/services/recents.js:
--------------------------------------------------------------------------------
1 | import * as StorageService from './storage';
2 | import { Recents } from '@common/constants';
3 |
4 | export async function loadRecentAddresses() {
5 | const recents = await StorageService.getItem(Recents.STORAGE_KEY);
6 | return recents ? JSON.parse(recents) : [];
7 | }
8 |
9 | export function saveRecentAddresses(recents) {
10 | return StorageService.setItem(Recents.STORAGE_KEY, JSON.stringify(recents));
11 | }
12 |
13 | export function removeRecentAddresses() {
14 | return StorageService.deleteItem(Recents.STORAGE_KEY);
15 | }
16 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | plugins: [
4 | [
5 | 'module-resolver',
6 | {
7 | alias: {
8 | '@assets': './assets',
9 | '@common': './src/common',
10 | '@components': './src/components'
11 | }
12 | }
13 | ],
14 | ['@babel/plugin-proposal-decorators', { legacy: true }],
15 | [
16 | '@babel/plugin-transform-runtime',
17 | {
18 | helpers: true,
19 | regenerator: false
20 | }
21 | ]
22 | ]
23 | };
24 |
--------------------------------------------------------------------------------
/src/common/actions/__tests__/general.js:
--------------------------------------------------------------------------------
1 | import * as Action from '../general';
2 |
3 | describe('GeneralActions', () => {
4 |
5 | it('should trigger a call for a notification in the view', async function() {
6 | const notificationDriverMock = { show: jest.fn(), LENGTH_SHORT: 1000 };
7 | try {
8 | await Action.notify('Test notification', 'short', notificationDriverMock);
9 | expect(notificationDriverMock.show).toBeCalledWith({ title: 'Test notification', duration: 1000 });
10 | } catch (e) {
11 | fail(e);
12 | }
13 | });
14 | });
--------------------------------------------------------------------------------
/src/components/widgets/InputWithIcon/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { InputWithIcon } from './index';
4 |
5 | describe.skip('', () => {
6 |
7 | it('should call the `onPressIcon` function when text was typed', () => {
8 | const onPressIcon = jest.fn();
9 | const wrapper = shallow();
10 |
11 | wrapper.childAt(0).simulate('changeText', 'abc');
12 | wrapper.childAt(1).simulate('press');
13 | expect(onPressIcon).toBeCalled();
14 |
15 | });
16 | });
--------------------------------------------------------------------------------
/src/components/widgets/HeaderIcon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, TouchableOpacity, View } from 'react-native';
3 | import { Icon } from '@components/widgets';
4 | import { measures } from '@common/styles';
5 |
6 | export const HeaderIcon = ({ onPress, ...props }) => (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | const styles = StyleSheet.create({
15 | container: {
16 | marginHorizontal: measures.defaultMargin * 2
17 | }
18 | });
--------------------------------------------------------------------------------
/src/components/views/SelectDestination/NoRecents.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { colors, measures } from '@common/styles';
4 |
5 | export default () => (
6 |
7 |
8 | You didn't sent anything yet.
9 |
10 |
11 | );
12 |
13 | const styles = StyleSheet.create({
14 | container: {
15 | alignItems: 'center',
16 | paddingTop: measures.defaultPadding
17 | },
18 | message: {
19 | color: colors.black
20 | }
21 | });
--------------------------------------------------------------------------------
/src/components/views/Settings/ListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
3 | import { measures } from '@common/styles';
4 |
5 | export default ({ children, onPress }) => (
6 |
7 |
8 |
9 | );
10 |
11 | const styles = StyleSheet.create({
12 | container: {
13 | height: 64,
14 | alignItems: 'stretch',
15 | justifyContent: 'center',
16 | padding: measures.defaultPadding
17 | }
18 | });
--------------------------------------------------------------------------------
/contracts/Migrations.sol:
--------------------------------------------------------------------------------
1 | pragma solidity >=0.4.21 <0.6.0;
2 |
3 | contract Migrations {
4 | address public owner;
5 | uint public last_completed_migration;
6 |
7 | constructor() public {
8 | owner = msg.sender;
9 | }
10 |
11 | modifier restricted() {
12 | if (msg.sender == owner) _;
13 | }
14 |
15 | function setCompleted(uint completed) public restricted {
16 | last_completed_migration = completed;
17 | }
18 |
19 | function upgrade(address new_address) public restricted {
20 | Migrations upgraded = Migrations(new_address);
21 | upgraded.setCompleted(last_completed_migration);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/views/ChangeCurrency/ListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
3 | import { measures } from '@common/styles';
4 |
5 | export default ({ children, onPress }) => (
6 |
7 |
8 |
9 | );
10 |
11 | const styles = StyleSheet.create({
12 | container: {
13 | height: 64,
14 | alignItems: 'stretch',
15 | justifyContent: 'center',
16 | padding: measures.defaultPadding
17 | }
18 | });
--------------------------------------------------------------------------------
/src/components/views/WalletSettings/ListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
3 | import { measures } from '@common/styles';
4 |
5 | export default ({ children, onPress }) => (
6 |
7 |
8 |
9 | );
10 |
11 | const styles = StyleSheet.create({
12 | container: {
13 | height: 64,
14 | alignItems: 'stretch',
15 | justifyContent: 'center',
16 | padding: measures.defaultPadding
17 | }
18 | });
--------------------------------------------------------------------------------
/src/components/views/WalletExtract/NoTransactions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { colors, measures } from '@common/styles';
4 |
5 | export default () => (
6 |
7 |
8 | There are still no transactions involving this wallet.
9 |
10 |
11 | );
12 |
13 | const styles = StyleSheet.create({
14 | container: {
15 | alignItems: 'center',
16 | paddingTop: measures.defaultPadding
17 | },
18 | message: {
19 | color: colors.black
20 | }
21 | });
--------------------------------------------------------------------------------
/src/common/services/wallets.js:
--------------------------------------------------------------------------------
1 | import * as StorageService from './storage';
2 | import { Wallet } from '@common/constants';
3 |
4 | export async function loadWalletPKs() {
5 | const pks = await StorageService.getItem(Wallet.STORAGE_KEY);
6 | return pks ? JSON.parse(pks) : [];
7 | }
8 |
9 | export async function saveWalletPKs(wallets) {
10 | const map = wallets.map(({ description, name, privateKey }) => ({ description, name, privateKey }));
11 | await StorageService.setItem(Wallet.STORAGE_KEY, JSON.stringify(map));
12 | }
13 |
14 | export function deleteWalletPKs() {
15 | return StorageService.deleteItem(Wallet.STORAGE_KEY);
16 | }
17 |
--------------------------------------------------------------------------------
/src/common/actions/recents.js:
--------------------------------------------------------------------------------
1 | import { Recents as RecentsService } from '@common/services';
2 | import { recents as RecentsStore } from '@common/stores';
3 |
4 | export async function loadRecents(store=RecentsStore, service=RecentsService) {
5 | store.isLoading(true);
6 | const recents = await service.loadRecentAddresses();
7 | store.loadAddresses(recents);
8 | store.isLoading(false);
9 | }
10 |
11 | export async function saveAddressToRecents(address, store=RecentsStore, service=RecentsService) {
12 | store.isLoading(true);
13 | store.addAddress(address);
14 | await service.saveRecentAddresses(store.list);
15 | store.isLoading(false);
16 | }
--------------------------------------------------------------------------------
/android/app/build_defs.bzl:
--------------------------------------------------------------------------------
1 | """Helper definitions to glob .aar and .jar targets"""
2 |
3 | def create_aar_targets(aarfiles):
4 | for aarfile in aarfiles:
5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
6 | lib_deps.append(":" + name)
7 | android_prebuilt_aar(
8 | name = name,
9 | aar = aarfile,
10 | )
11 |
12 | def create_jar_targets(jarfiles):
13 | for jarfile in jarfiles:
14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
15 | lib_deps.append(":" + name)
16 | prebuilt_jar(
17 | name = name,
18 | binary_jar = jarfile,
19 | )
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/components/widgets/Button/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from 'react-native';
3 | import { shallow } from 'enzyme';
4 | import { Button } from './index';
5 |
6 | describe('', () => {
7 |
8 | it('should have a label `button label`', () => {
9 | const wrapper = shallow();
10 | expect(wrapper.find(Text).children().text()).toEqual('button label');
11 | });
12 |
13 | it('should call the `onPress` function when clicked', () => {
14 | const onPress = jest.fn();
15 | const wrapper = shallow();
16 | wrapper.simulate('press');
17 | expect(onPress).toBeCalled();
18 | });
19 | });
--------------------------------------------------------------------------------
/src/components/views/index.js:
--------------------------------------------------------------------------------
1 | export * from './ChangeCurrency';
2 | export * from './ConfirmMnemonics';
3 | export * from './ConfirmTransaction';
4 | export * from './CreateMnemonics';
5 | export * from './CreateWallet';
6 | export * from './LoadMnemonics';
7 | export * from './LoadPrivateKey';
8 | export * from './LoadWallet';
9 | export * from './NewWallet';
10 | export * from './NewWalletName';
11 | export * from './ReceiveCoins';
12 | export * from './SendCoins';
13 | export * from './Settings';
14 | export * from './ShowPrivateKey';
15 | export * from './SelectDestination';
16 | export * from './WalletDetails';
17 | export * from './WalletExtract';
18 | export * from './WalletSettings';
19 | export * from './WalletsOverview';
20 |
--------------------------------------------------------------------------------
/src/components/widgets/TextBullet/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { colors, measures } from '@common/styles';
4 |
5 | export const TextBullet = ({ children }) => (
6 |
7 | {children}
8 |
9 | );
10 |
11 | const styles = StyleSheet.create({
12 | container: {
13 | backgroundColor: colors.secondary,
14 | padding: measures.defaultPadding,
15 | borderWidth: 1,
16 | borderColor: colors.primary,
17 | borderRadius: 4
18 | },
19 | label: {
20 | color: colors.primary,
21 | fontSize: 16,
22 | fontWeight: '600'
23 | },
24 | });
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/ios/Partnr/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/contracts/MultiSigWalletFactory.sol:
--------------------------------------------------------------------------------
1 | pragma solidity >=0.4.22 < 0.6.0;
2 | import "./Factory.sol";
3 | import "./MultiSignatureWallet.sol";
4 |
5 |
6 | /// @title Multisignature wallet factory - Allows creation of multisig wallet.
7 | /// @author Stefan George -
8 | contract MultiSigWalletFactory is Factory {
9 |
10 | /*
11 | * Public functions
12 | */
13 | /// @dev Allows verified creation of multisignature wallet.
14 | /// @param _owners List of initial owners.
15 | /// @param _required Number of required confirmations.
16 | /// @return Returns wallet address.
17 | function create(address[] memory _owners, uint _required) public returns (address wallet) {
18 | wallet = new MultiSigWallet( _owners, _required);
19 | register(wallet);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ios/PartnrTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/common/services/transactions.js:
--------------------------------------------------------------------------------
1 | import ethers from 'ethers';
2 | import { Transaction as TransactionUtils, Wallet as WalletUtils } from '@common/utils';
3 |
4 | const { Wallet, utils } = ethers;
5 |
6 | export function sendTransaction(wallet, transaction) {
7 | if (!(wallet instanceof Wallet)) throw new Error('Invalid wallet');
8 | if (!TransactionUtils.isValidTransaction(transaction)) throw new Error('Invalid transaction');
9 | return wallet.sendTransaction(transaction);
10 | }
11 |
12 | export function sendEther(wallet, destination, amount, options) {
13 | if (!(wallet instanceof Wallet)) throw new Error('Invalid wallet');
14 | if (typeof destination !== 'string') throw new Error('Invalid destination address');
15 | if (!(amount instanceof utils.BigNumber)) amount = utils.parseEther(amount);
16 | return wallet.send(destination, amount, options);
17 | }
--------------------------------------------------------------------------------
/ios/Partnr-tvOSTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/common/stores/recents.js:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 |
3 | const INITIAL = {
4 | list: [],
5 | loading: false
6 | };
7 |
8 | export class RecentsStore {
9 |
10 | @observable list = INITIAL.list;
11 | @observable loading = INITIAL.loading;
12 |
13 | @action isLoading(state) {
14 | this.loading = Boolean(state);
15 | }
16 |
17 | @action addAddress(address) {
18 | const index = this.list.findIndex(a => a === address);
19 | if (index > -1) return;
20 | this.list.push(address);
21 | }
22 |
23 | @action loadAddresses(addresses) {
24 | this.list = [];
25 | addresses.forEach(address => this.addAddress(address));
26 | }
27 |
28 | @action reset() {
29 | this.list = INITIAL.list;
30 | this.loading = INITIAL.loading;
31 | }
32 | }
33 |
34 | export default new RecentsStore();
--------------------------------------------------------------------------------
/src/components/widgets/Button/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3 | import { colors, measures } from '@common/styles';
4 |
5 | export const Button = ({ children, onPress }) => (
6 |
7 |
8 |
9 | );
10 |
11 | const styles = StyleSheet.create({
12 | container: {
13 | height: 48,
14 | flexDirection: 'row',
15 | justifyContent: 'center',
16 | alignItems: 'center',
17 | borderWidth: 2,
18 | backgroundColor: colors.primary,
19 | borderColor: colors.secondary,
20 | padding: measures.defaultPadding,
21 | borderRadius: 4
22 | },
23 | title: {
24 | color: colors.secondary,
25 | fontSize: 16
26 | }
27 | });
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
--------------------------------------------------------------------------------
/src/components/views/SendCoins/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { Button, Calculator } from '@components/widgets';
4 | import { colors } from '@common/styles';
5 |
6 | export class SendCoins extends React.Component {
7 |
8 | onPressContinue() {
9 | const { amount } = this.refs.calc;
10 | if (!amount) return;
11 | this.props.navigation.navigate('SelectDestination', { amount });
12 | }
13 |
14 | render() {
15 | return (
16 |
17 |
18 |
20 | );
21 | }
22 | }
23 |
24 | const styles = StyleSheet.create({
25 | container: {
26 | backgroundColor: colors.defaultBackground,
27 | flex: 1,
28 | alignItems: 'stretch'
29 | }
30 | });
--------------------------------------------------------------------------------
/src/common/utils/transaction.js:
--------------------------------------------------------------------------------
1 | import ethers from 'ethers';
2 |
3 | const { utils } = ethers;
4 |
5 | const DEFAULT_GASLIMIT = 21000;
6 | // const DEFAULT_GASLIMIT = 200000;
7 | const DEFAULT_GASPRICE = 4000000000; // 4 gwei
8 | // const DEFAULT_GASPRICE = 60000000000; // 60 gwei
9 |
10 | export function createTransaction(to, value, gasLimit = DEFAULT_GASLIMIT, options = {}) {
11 | if (!value) throw new Error('The transaction value is required.');
12 | else if (!(Number(value) > 0)) throw new Error('The transaction value is invalid.');
13 | else if (isNaN(gasLimit)) gasLimit = DEFAULT_GASLIMIT;
14 | const gasPrice = DEFAULT_GASPRICE;
15 | value = utils.parseEther(value);
16 | return { gasPrice, ...options, to, gasLimit, value };
17 | }
18 |
19 | export function isValidTransaction(transaction) {
20 | return transaction instanceof Object
21 | && Number(transaction.value) > 0 && Number(transaction.gasLimit) > 0 && typeof transaction.to === 'string';
22 | }
--------------------------------------------------------------------------------
/src/components/views/WalletDetails/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TabView } from '@components/widgets';
3 | import { ReceiveCoins, SendCoins, WalletExtract, WalletSettings } from '..';
4 |
5 | export class WalletDetails extends React.Component {
6 |
7 | static navigationOptions = ({ navigation }) => ({
8 | title: navigation.state.params.wallet.name
9 | });
10 |
11 | tabs = [
12 | { id: 'extract', label: 'Extract', icon: 'list', content: },
13 | { id: 'receive', label: 'Receive', icon: 'qrcode', type: 'fa', content: },
14 | { id: 'send', label: 'Send', icon: 'cube-send', type: 'mdc', content: },
15 | { id: 'settings', label: 'Settings', icon: 'settings', content: }
16 | ];
17 |
18 | render() {
19 | return ;
20 | }
21 | }
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "28.0.3"
6 | minSdkVersion = 16
7 | compileSdkVersion = 28
8 | targetSdkVersion = 28
9 | supportLibVersion = "28.0.0"
10 | }
11 | repositories {
12 | google()
13 | jcenter()
14 | }
15 | dependencies {
16 | classpath("com.android.tools.build:gradle:3.4.0")
17 |
18 | // NOTE: Do not place your application dependencies here; they belong
19 | // in the individual module build.gradle files
20 | }
21 | }
22 |
23 | allprojects {
24 | repositories {
25 | mavenLocal()
26 | google()
27 | jcenter()
28 | maven {
29 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
30 | url "$rootDir/../node_modules/react-native/android"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/src/common/actions/transactions.js:
--------------------------------------------------------------------------------
1 | import ethers from 'ethers';
2 | import { notify } from './general';
3 | import { wallet as WalletStore } from '@common/stores';
4 | import { Transactions as TransactionsService } from '@common/services';
5 |
6 | async function waitForTransaction(wallet, txn) {
7 | await wallet.provider.waitForTransaction(txn.hash);
8 | WalletStore.moveToHistory(txn);
9 | notify('Transaction confirmed');
10 | }
11 |
12 | export async function sendEther(wallet, destination, amount, options) {
13 | const txn = await TransactionsService.sendEther(wallet, destination, amount, options);
14 | WalletStore.addPendingTransaction(txn);
15 | waitForTransaction(wallet, txn);
16 | return txn;
17 | }
18 |
19 | export async function sendTransaction(wallet, txn) {
20 | if (!(wallet instanceof ethers.Wallet)) throw new Error('Invalid wallet');
21 | txn = await TransactionsService.sendTransaction(wallet, txn);
22 | WalletStore.addPendingTransaction(txn);
23 | waitForTransaction(wallet, txn);
24 | return txn;
25 | }
--------------------------------------------------------------------------------
/src/components/views/ConfirmTransaction/ErrorMessage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { colors, measures } from '@common/styles';
4 |
5 | const styles = StyleSheet.create({
6 | container: {
7 | backgroundColor: colors.errorAlt,
8 | flexDirection: 'column',
9 | alignItems: 'center',
10 | justifyContent: 'space-around',
11 | paddingVertical: measures.defaultPadding,
12 | marginVertical: measures.defaultMargin
13 | },
14 | title: {
15 | fontWeight: 'bold',
16 | fontSize: measures.fontSizeMedium,
17 | color: colors.error,
18 | textAlign: 'center'
19 | },
20 | message: {
21 | fontSize: measures.fontSizeMedium - 2,
22 | color: colors.error,
23 | textAlign: 'center'
24 | }
25 | });
26 |
27 | export default ({ error }) => !error ? null : (
28 |
29 | Transaction failed
30 | {error.message}
31 |
32 | );
--------------------------------------------------------------------------------
/src/components/widgets/TabView/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import TabBar from './TabBar';
4 |
5 | export class TabView extends React.Component {
6 |
7 | state = { active: 0 };
8 |
9 | onPressItem(id) {
10 | const active = this.props.tabs.findIndex(tab => tab.id === id);
11 | this.setState({ active });
12 | }
13 |
14 | render() {
15 | const { tabs } = this.props;
16 | const { active } = this.state;
17 | return (
18 |
19 |
20 | this.onPressItem(id)} />
21 |
22 | );
23 | }
24 | }
25 |
26 | const styles = StyleSheet.create({
27 | container: {
28 | flex: 1,
29 | alignItems: 'stretch',
30 | flexDirection: 'column'
31 | },
32 | body: {
33 | flex: 1
34 | }
35 | });
--------------------------------------------------------------------------------
/src/common/styles/colors.js:
--------------------------------------------------------------------------------
1 | export const black = '#000000';
2 | export const darkGray = '#1E1E1E';
3 | export const gray = '#808080';
4 | export const lightGray = '#CCCCCC';
5 | export const lightestGray = '#EFEFEF';
6 | export const white = '#FFFFFF';
7 | export const green = '#689f38';
8 | export const lightGreen = '#c5e1a5';
9 | export const red = '#b71c1c';
10 | export const lightRed = '#ef9a9a';
11 |
12 | export const primary = darkGray;
13 | export const secondary = white;
14 |
15 | export const statusBar = primary;
16 | export const defaultBackground = white;
17 | export const splashscreenBackground = darkGray;
18 |
19 | export const success = green;
20 | export const successAlt = lightGreen;
21 | export const error = red;
22 | export const errorAlt = lightRed;
23 | export const pending = gray;
--------------------------------------------------------------------------------
/src/components/widgets/Icon/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { Icon } from './index';
4 | import { measures } from '@common/styles';
5 |
6 | describe('', () => {
7 |
8 | it('should be null if no name given', () => {
9 | const wrapper = shallow();
10 | expect(wrapper.getElement()).toBeNull();
11 | });
12 |
13 | it('should receive props properly', () => {
14 | const wrapper = shallow();
15 | const props = wrapper.props();
16 | expect(props.color).toEqual('red');
17 | expect(props.name).toEqual('ios-add');
18 | expect(props.size).toEqual(measures.iconSizeLarge);
19 | });
20 |
21 | it('should have default values when props not given', () => {
22 | const wrapper = shallow();
23 | const props = wrapper.props();
24 | expect(props.color).toEqual('#000000');
25 | expect(props.size).toEqual(measures.iconSizeMedium);
26 | });
27 | });
--------------------------------------------------------------------------------
/src/components/widgets/TabView/TabBarIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
3 | import { Icon } from '@components/widgets';
4 | import { colors } from '@common/styles';
5 |
6 | const getLabelColor = (active) => active ? styles.activeLabel : styles.label;
7 |
8 | export default ({ active, icon, label, onPress, ...props }) => (
9 |
10 |
11 |
12 | {label}
13 |
14 |
15 | );
16 |
17 | const styles = StyleSheet.create({
18 | container: {
19 | alignItems: 'center',
20 | justifyContent: 'center',
21 | flexDirection: 'column',
22 | flex: 1
23 | },
24 | activeLabel: {
25 | color: colors.black
26 | },
27 | label: {
28 | color: colors.gray
29 | }
30 | });
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 |
33 | # node.js
34 | #
35 | node_modules/
36 | npm-debug.log
37 | yarn-error.log
38 |
39 | # BUCK
40 | buck-out/
41 | \.buckd/
42 | *.keystore
43 |
44 | # fastlane
45 | #
46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47 | # screenshots whenever they are needed.
48 | # For more information about the recommended setup visit:
49 | # https://docs.fastlane.tools/best-practices/source-control/
50 |
51 | */fastlane/report.xml
52 | */fastlane/Preview.html
53 | */fastlane/screenshots
54 |
55 | *.apk
56 |
57 | # Bundle artifact
58 | *.jsbundle
59 |
--------------------------------------------------------------------------------
/src/components/widgets/TabView/TabBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { colors } from '@common/styles';
4 | import TabBarIcon from './TabBarIcon';
5 |
6 | export default class TabBar extends React.Component {
7 |
8 | renderTab = (tab, i) => (
9 | this.props.onPressTabItem(tab.id)} />
14 | )
15 |
16 | render() {
17 | const { tabs } = this.props;
18 | return (
19 |
20 | {tabs.map(this.renderTab)}
21 |
22 | );
23 | }
24 | }
25 |
26 | const styles = StyleSheet.create({
27 | container: {
28 | alignItems: 'stretch',
29 | justifyContent: 'space-around',
30 | flexDirection: 'row',
31 | height: 48,
32 | borderTopWidth: 1,
33 | borderColor: colors.gray,
34 | backgroundColor: colors.lightestGray
35 | }
36 | });
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'Partnr'
2 | include ':react-native-vector-icons'
3 | project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
4 | include ':react-native-svg'
5 | project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
6 | include ':react-native-snackbar'
7 | project(':react-native-snackbar').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-snackbar/android')
8 | include ':react-native-sensitive-info'
9 | project(':react-native-sensitive-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sensitive-info/android')
10 | include ':react-native-gesture-handler'
11 | project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
12 | include ':react-native-camera'
13 | project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android')
14 |
15 | include ':app'
16 |
--------------------------------------------------------------------------------
/src/common/utils/shims.js:
--------------------------------------------------------------------------------
1 | if (!Uint8Array.prototype.slice) {
2 | Uint8Array.prototype.slice = function() {
3 | var args = Array.prototype.slice.call(arguments);
4 | return new Uint8Array(Array.prototype.slice.apply(this, args));
5 | }
6 | }
7 |
8 | Array.prototype.equals = function (array) {
9 | // if the other array is a falsy value, return
10 | if (!array)
11 | return false;
12 |
13 | // compare lengths - can save a lot of time
14 | if (this.length != array.length)
15 | return false;
16 |
17 | for (var i = 0, l=this.length; i < l; i++) {
18 | // Check if we have nested arrays
19 | if (this[i] instanceof Array && array[i] instanceof Array) {
20 | // recurse into the nested arrays
21 | if (!this[i].equals(array[i]))
22 | return false;
23 | }
24 | else if (this[i] != array[i]) {
25 | // Warning - two different object instances will never be equal: {x:20} != {x:20}
26 | return false;
27 | }
28 | }
29 | return true;
30 | }
--------------------------------------------------------------------------------
/src/components/views/ConfirmTransaction/SuccessMessage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { colors, measures } from '@common/styles';
4 |
5 | const styles = StyleSheet.create({
6 | container: {
7 | backgroundColor: colors.successAlt,
8 | flexDirection: 'column',
9 | alignItems: 'center',
10 | justifyContent: 'space-around',
11 | paddingVertical: measures.defaultPadding,
12 | marginVertical: measures.defaultMargin
13 | },
14 | title: {
15 | fontWeight: 'bold',
16 | fontSize: measures.fontSizeMedium,
17 | color: colors.success,
18 | textAlign: 'center'
19 | },
20 | message: {
21 | fontSize: measures.fontSizeMedium - 2,
22 | color: colors.success,
23 | textAlign: 'center'
24 | }
25 | });
26 |
27 | export default ({ txn }) => (!txn || !txn.hash) ? null : (
28 |
29 | Transaction successful
30 |
31 | Your transaction was sent successfully and now is waiting for confirmation. Please wait.
32 |
33 |
34 | );
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/node:12.10
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/mongo:3.4.4
16 |
17 | working_directory: ~/src
18 |
19 | steps:
20 | - checkout
21 |
22 | # Download and cache dependencies
23 | - restore_cache:
24 | keys:
25 | - v1-dependencies-{{ checksum "package.json" }}
26 | # fallback to using the latest cache if no exact match is found
27 | - v1-dependencies-
28 |
29 | - run: yarn install
30 |
31 | - save_cache:
32 | paths:
33 | - node_modules
34 | key: v1-dependencies-{{ checksum "package.json" }}
35 |
36 | # run tests!
37 | - run: yarn test --maxWorkers=2 --ci
--------------------------------------------------------------------------------
/src/common/actions/general.js:
--------------------------------------------------------------------------------
1 | import Snackbar from 'react-native-snackbar';
2 | import { Recents as RecentsService, Wallets as WalletsService } from '@common/services';
3 | import * as store from '@common/stores';
4 |
5 | export async function notify(title, duration, driver=Snackbar) {
6 | switch (duration) {
7 |
8 | case 'long':
9 | duration = driver.LENGTH_LONG;
10 | break;
11 |
12 | case 'indefinite':
13 | duration = driver.LENGTH_INDEFINITE;
14 | break;
15 |
16 | case 'short':
17 | default:
18 | duration = driver.LENGTH_SHORT;
19 | break;
20 | }
21 |
22 | driver.show({ title, duration });
23 | }
24 |
25 | export async function eraseAllData() {
26 | await cleanStorage();
27 | cleanStores();
28 | }
29 |
30 | function cleanStorage() {
31 | return [
32 | RecentsService.removeRecentAddresses(),
33 | WalletsService.deleteWalletPKs()
34 | ];
35 | }
36 |
37 | function cleanStores() {
38 | store.prices.reset();
39 | store.recents.reset();
40 | store.wallet.reset();
41 | store.wallets.reset();
42 | }
--------------------------------------------------------------------------------
/src/components/views/ShowPrivateKey/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import QRCode from 'react-native-qrcode-svg';
4 | import { colors, measures } from '@common/styles';
5 |
6 | export class ShowPrivateKey extends React.Component {
7 |
8 | static navigationOptions = { title: 'Private key' };
9 |
10 | render() {
11 | const { wallet: { item } } = this.props.navigation.state.params;
12 | return (
13 |
14 |
15 |
16 |
17 | {item.privateKey}
18 |
19 | );
20 | }
21 | }
22 |
23 | const styles = StyleSheet.create({
24 | container: {
25 | backgroundColor: colors.white,
26 | flex: 1,
27 | alignItems: 'stretch',
28 | justifyContent: 'space-around',
29 | padding: measures.defaultPadding
30 | },
31 | centered: {
32 | alignSelf: 'center',
33 | textAlign: 'center'
34 | }
35 | });
--------------------------------------------------------------------------------
/src/components/widgets/Calculator/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { NumberGrid } from '@components/widgets';
4 | import { colors } from '@common/styles';
5 | import Panel from './Panel';
6 |
7 | export class Calculator extends React.Component {
8 |
9 | get amount() {
10 | return this.refs.panel.wrappedInstance.amount;
11 | }
12 |
13 | onPressNumber(number) {
14 | this.refs.panel.wrappedInstance.onChange(number);
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
21 |
22 | this.onPressNumber(number)} />
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | const styles = StyleSheet.create({
30 | container: {
31 | backgroundColor: colors.defaultBackground,
32 | flex: 5,
33 | alignItems: 'stretch'
34 | },
35 | bottomContainer: {
36 | flex: 4,
37 | alignItems: 'stretch',
38 | justifyContent: 'center'
39 | }
40 | });
--------------------------------------------------------------------------------
/contracts/Factory.sol:
--------------------------------------------------------------------------------
1 | pragma solidity >=0.4.22 < 0.6.0;
2 |
3 | contract Factory {
4 |
5 | /*
6 | * Events
7 | */
8 | event ContractInstantiation(address sender, address instantiation);
9 |
10 | /*
11 | * Storage
12 | */
13 | mapping(address => bool) public isInstantiation;
14 | mapping(address => address[]) public instantiations;
15 |
16 | /*
17 | * Public functions
18 | */
19 | /// @dev Returns number of instantiations by creator.
20 | /// @param creator Contract creator.
21 | /// @return Returns number of instantiations by creator.
22 | function getInstantiationCount(address creator)
23 | public
24 | view
25 | returns (uint)
26 | {
27 | return instantiations[creator].length;
28 | }
29 |
30 | /*
31 | * Internal functions
32 | */
33 | /// @dev Registers contract in factory registry.
34 | /// @param instantiation Address of contract instantiation.
35 | function register(address instantiation)
36 | internal
37 | {
38 | isInstantiation[instantiation] = true;
39 | instantiations[msg.sender].push(instantiation);
40 | emit ContractInstantiation(msg.sender, instantiation);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/common/stores/prices.js:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 |
3 | const INITIAL = {
4 | usd: 0,
5 | eur: 0,
6 | jmd: 0,
7 | loading: false
8 | };
9 |
10 | export class PricesStore {
11 |
12 | @observable usd = INITIAL.usd;
13 | @observable eur = INITIAL.eur;
14 | @observable jmd = INITIAL.jmd;
15 | @observable loading = INITIAL.loading;
16 |
17 | validateInput(input) {
18 | if (isNaN(input) || typeof input !== 'number') throw new Error('The input is NaN');
19 | }
20 |
21 | @action isLoading(state) {
22 | this.loading = Boolean(state);
23 | }
24 |
25 | @action setUSDRate(rate) {
26 | this.validateInput(rate);
27 | this.usd = Number(rate);
28 | }
29 |
30 | @action setEURRate(rate) {
31 | this.validateInput(rate);
32 | this.eur = Number(rate);
33 | }
34 |
35 | @action setJMDRate(rate) {
36 | this.validateInput(rate);
37 | this.jmd = Number(rate);
38 | }
39 |
40 | @action reset() {
41 | this.usd = INITIAL.usd;
42 | this.eur = INITIAL.eur;
43 | this.jmd = INITIAL.jmd;
44 | this.loading = INITIAL.loading;
45 | }
46 | }
47 |
48 | export default new PricesStore();
--------------------------------------------------------------------------------
/src/common/stores/wallets.js:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 | import ethers from 'ethers';
3 |
4 | const INITIAL = {
5 | list: [],
6 | loading: false
7 | };
8 |
9 | export class WalletsStore {
10 |
11 | @observable list = INITIAL.list;
12 | @observable loading = INITIAL.loading;
13 |
14 | @action isLoading(state) {
15 | this.loading = Boolean(state);
16 | }
17 |
18 | @action addWallet(walletName, wallet, walletDescription = '') {
19 | if (!(wallet instanceof ethers.Wallet)) throw new Error('Invalid Wallet');
20 | wallet.name = walletName;
21 | wallet.description = walletDescription;
22 | this.list.push(wallet);
23 | }
24 |
25 | @action removeWallet(wallet) {
26 | this.list = this.list.filter(w => w.getAddress() !== wallet.getAddress());
27 | }
28 |
29 | @action setBalance(address, amount) {
30 | const wallet = this.list.find(wallet => wallet.getAddress() === address);
31 | if (!wallet) throw new Error('Wallet not found');
32 | wallet.balance = amount;
33 | const otherWallets = this.list.filter(wallet => wallet.getAddress() !== address);
34 | this.list = [...otherWallets, wallet];
35 | }
36 |
37 | @action reset() {
38 | this.list = INITIAL.list;
39 | this.loading = INITIAL.loading;
40 | }
41 | }
42 |
43 | export default new WalletsStore();
--------------------------------------------------------------------------------
/android/app/BUCK:
--------------------------------------------------------------------------------
1 | # To learn about Buck see [Docs](https://buckbuild.com/).
2 | # To run your application with Buck:
3 | # - install Buck
4 | # - `npm start` - to start the packager
5 | # - `cd android`
6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
8 | # - `buck install -r android/app` - compile, install and run application
9 | #
10 |
11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
12 |
13 | lib_deps = []
14 |
15 | create_aar_targets(glob(["libs/*.aar"]))
16 |
17 | create_jar_targets(glob(["libs/*.jar"]))
18 |
19 | android_library(
20 | name = "all-libs",
21 | exported_deps = lib_deps,
22 | )
23 |
24 | android_library(
25 | name = "app-code",
26 | srcs = glob([
27 | "src/main/java/**/*.java",
28 | ]),
29 | deps = [
30 | ":all-libs",
31 | ":build_config",
32 | ":res",
33 | ],
34 | )
35 |
36 | android_build_config(
37 | name = "build_config",
38 | package = "com.partnr",
39 | )
40 |
41 | android_resource(
42 | name = "res",
43 | package = "com.partnr",
44 | res = "src/main/res",
45 | )
46 |
47 | android_binary(
48 | name = "app",
49 | keystore = "//android/keystores:debug",
50 | manifest = "src/main/AndroidManifest.xml",
51 | package_type = "debug",
52 | deps = [
53 | ":app-code",
54 | ],
55 | )
56 |
--------------------------------------------------------------------------------
/contracts/Escrow.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.5.1;
2 |
3 | contract Escrow {
4 | // Parameters of the total savings.
5 | // or time periods in seconds.
6 | address payable public beneficiary;
7 |
8 | // absolute unix timestamps (seconds since 1970-01-01)
9 | uint256 public paymentTime;
10 |
11 | // Final savings goal.
12 | uint public totalRequestedSavings;
13 |
14 |
15 | // Set to true at the end, disallows any change.
16 | // By default initialized to `false`.
17 | bool ended;
18 |
19 |
20 | //Events that will be emitted on changes
21 | event paymentDispatched(address beneficiary, uint amount);
22 | event totalRequestedSavingsIncreased(uint amount);
23 | event totalRequestedSavingsDecreased(uint amount);
24 |
25 | /// Create a simple auction with `_paymentTime`
26 | /// days, months or weeks time on behalf of the
27 | /// beneficiary address `_beneficiary`.
28 | constructor(
29 | uint _paymentTime,
30 | address payable _beneficiary,
31 | uint _totalRequestedSavings
32 | ) public {
33 | require(_paymentTime > now , "Cannot accept past date");
34 | beneficiary = _beneficiary;
35 | paymentTime = _paymentTime;
36 | totalRequestedSavings = _totalRequestedSavings;
37 | }
38 |
39 | function release() public {
40 | require(now >= paymentTime, "Funds are not ready to be released");
41 | address(beneficiary).transfer(totalRequestedSavings);
42 | emit paymentDispatched(beneficiary, totalRequestedSavings);
43 | }
44 | }
--------------------------------------------------------------------------------
/ios/Partnr/AppDelegate.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | #import "AppDelegate.h"
9 |
10 | #import
11 | #import
12 | #import
13 |
14 | @implementation AppDelegate
15 |
16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
17 | {
18 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
19 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
20 | moduleName:@"Partnr"
21 | initialProperties:nil];
22 |
23 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
24 |
25 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
26 | UIViewController *rootViewController = [UIViewController new];
27 | rootViewController.view = rootView;
28 | self.window.rootViewController = rootViewController;
29 | [self.window makeKeyAndVisible];
30 | return YES;
31 | }
32 |
33 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
34 | {
35 | #if DEBUG
36 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
37 | #else
38 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
39 | #endif
40 | }
41 |
42 | @end
43 |
--------------------------------------------------------------------------------
/src/common/actions/wallets.js:
--------------------------------------------------------------------------------
1 | import { wallet as WalletStore, wallets as WalletsStore } from '@common/stores';
2 | import { Wallets as WalletsService, Api as ApiService } from '@common/services';
3 | import { Wallet as WalletUtils } from '@common/utils';
4 |
5 | export async function addWallet(walletName, wallet, walletDescription = '') {
6 | WalletsStore.isLoading(true);
7 | WalletsStore.addWallet(walletName, wallet, walletDescription);
8 | WalletsStore.isLoading(false);
9 | }
10 |
11 | export async function loadWallets() {
12 | WalletsStore.isLoading(true);
13 | const pks = await WalletsService.loadWalletPKs();
14 | pks.map(({ description, name, privateKey }) => {
15 | const wallet = WalletUtils.loadWalletFromPrivateKey(privateKey);
16 | WalletsStore.addWallet(name, wallet, description);
17 | });
18 | WalletsStore.isLoading(false);
19 | }
20 |
21 | export async function updateBalance(wallet) {
22 | const balance = await wallet.getBalance();
23 | WalletsStore.setBalance(wallet.getAddress(), balance);
24 | }
25 |
26 | export async function removeWallet(wallet) {
27 | WalletsStore.removeWallet(wallet);
28 | }
29 |
30 | export async function saveWallets() {
31 | await WalletsService.saveWalletPKs(WalletsStore.list);
32 | }
33 |
34 | export async function selectWallet(wallet) {
35 | WalletStore.select(wallet);
36 | }
37 |
38 | export async function updateHistory(wallet) {
39 | WalletStore.isLoading(true);
40 | const { data } = await ApiService.getHistory(wallet.getAddress());
41 | if (data.status == 1) WalletStore.setHistory(data.result);
42 | WalletStore.isLoading(false);
43 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BackHandler, StatusBar, StyleSheet, View } from 'react-native';
3 | import { Provider } from 'mobx-react';
4 | import { colors } from './common/styles';
5 | import Router, { INITIAL_ROUTE } from './Router';
6 | import * as stores from './common/stores';
7 |
8 | const STATUSBAR_CONFIG = {
9 | backgroundColor: colors.statusBar,
10 | barStyle: 'light-content',
11 | translucent: false
12 | };
13 |
14 | export default class Application extends React.Component {
15 |
16 | componentDidMount() {
17 | BackHandler.addEventListener('hardwareBackPress', () => this.handleBackButton());
18 | }
19 |
20 | componentWillUnmount() {
21 | BackHandler.removeEventListener('hardwareBackPress');
22 | }
23 |
24 | handleBackButton() {
25 | if (!this.props.navigation) return false;
26 |
27 | const { state, goBack } = this.props.navigation;
28 | if (state.routes.length > 1 && state.index > 0) {
29 | goBack();
30 | return true;
31 | }
32 | return false;
33 | }
34 |
35 | render() {
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 | }
46 |
47 | const styles = StyleSheet.create({
48 | container: {
49 | backgroundColor: colors.defaultBackground,
50 | flex: 1,
51 | alignItems: 'stretch',
52 | justifyContent: 'center'
53 | }
54 | });
--------------------------------------------------------------------------------
/src/common/stores/wallet.js:
--------------------------------------------------------------------------------
1 | import { action, observable } from 'mobx';
2 | import { Transaction as TransactionUtils } from '@common/utils';
3 | import ethers from 'ethers';
4 |
5 | const INITIAL = {
6 | item: null,
7 | history: [],
8 | pendingTransactions: [],
9 | loading: false
10 | };
11 |
12 | export class WalletStore {
13 |
14 | @observable item = INITIAL.item;
15 | @observable history = INITIAL.history;
16 | @observable pendingTransactions = INITIAL.pendingTransactions;
17 | @observable loading = INITIAL.loading;
18 |
19 | @action isLoading(state) {
20 | this.loading = Boolean(state);
21 | }
22 |
23 | @action select(wallet) {
24 | if (!(wallet instanceof ethers.Wallet)) throw new Error('Invalid Wallet');
25 | this.item = wallet;
26 | }
27 |
28 | @action setHistory(history) {
29 | if (!this.item) throw new Error(`Can't update the history. No wallet was selected.`);
30 | if (!(history instanceof Array)) throw new Error('The history must be an array.');
31 | this.history = history;
32 | }
33 |
34 | @action addPendingTransaction(txn) {
35 | this.pendingTransactions.push(txn);
36 | }
37 |
38 | @action moveToHistory(txn) {
39 | const pending = this.pendingTransactions.filter(tx => txn.hash !== tx.hash);
40 | this.pendingTransactions = pending;
41 | this.history.push(txn);
42 | }
43 |
44 | @action reset() {
45 | this.item = INITIAL.item;
46 | this.history = INITIAL.history;
47 | this.pendingTransactions = INITIAL.pendingTransactions;
48 | this.loading = INITIAL.loading;
49 | }
50 | }
51 |
52 | export default new WalletStore();
--------------------------------------------------------------------------------
/src/components/views/LoadWallet/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { Button } from '@components/widgets';
4 | import { colors, measures } from '@common/styles';
5 |
6 | export class LoadWallet extends React.Component {
7 |
8 | static navigationOptions = { title: 'Load Wallet' };
9 |
10 | render() {
11 | const { navigate, state: { params: { walletName, walletDescription } } } = this.props.navigation;
12 | return (
13 |
14 | Load the wallet from
15 |
16 |
23 |
24 | );
25 | }
26 | }
27 |
28 | const styles = StyleSheet.create({
29 | container: {
30 | backgroundColor: colors.defaultBackground,
31 | alignItems: 'stretch',
32 | justifyContent: 'space-between',
33 | flex: 1,
34 | padding: measures.defaultPadding,
35 | },
36 | message: {
37 | color: colors.black,
38 | fontSize: 16,
39 | textAlign: 'center',
40 | margin: measures.defaultMargin * 4,
41 | },
42 | buttonsContainer: {
43 | justifyContent: 'space-between'
44 | }
45 | });
--------------------------------------------------------------------------------
/src/components/views/WalletsOverview/NoWallets.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react';
2 | import {StyleSheet, View} from 'react-native';
3 | import { Body, Button, Content, Card, CardItem, Fab, Text } from 'native-base';
4 | import Icon from 'react-native-vector-icons/FontAwesome5';
5 |
6 |
7 | export default (navigation) => {
8 | const [active, setActive] = useState(false);
9 |
10 | const onPress = () => {
11 | setActive(!active);
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 |
19 | Oops! Looks like you have no Wallets :(
20 |
21 |
22 |
23 | Click the button below to add one :)
24 |
25 |
26 |
27 |
28 | onPress()}>
35 |
36 |
37 | navigation.navigate('NewWalletName')}/>
38 |
39 |
40 |
41 | );
42 | }
43 |
44 |
45 | const styles = StyleSheet.create({
46 | container: {
47 | flex: 1
48 | }
49 | });
--------------------------------------------------------------------------------
/src/components/views/ChangeCurrency/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ScrollView, StyleSheet, Text, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { colors, measures } from '@common/styles';
5 | import ListItem from './ListItem';
6 |
7 | @inject('prices')
8 | @observer
9 | export class ChangeCurrency extends React.Component {
10 |
11 | static navigationOptions = ({ navigation, screenProps }) => ({
12 | title: 'Select currency'
13 | });
14 |
15 | selectCurrency(currency) {
16 |
17 | }
18 |
19 | renderItems = (items) => items.map((item, index) => (
20 |
21 |
22 | {item.title}
23 |
24 |
25 | ));
26 |
27 | render() {
28 | return (
29 |
30 | {this.renderItems([
31 | { title: 'Jamaican Dollar', action: () => this.selectCurrency('jmd') },
32 | { title: 'Dollar', action: () => this.selectCurrency('usd') },
33 | { title: 'Euro', action: () => this.selectCurrency('eur') },
34 | ])}
35 |
36 | );
37 | }
38 | }
39 |
40 | const styles = StyleSheet.create({
41 | container: {
42 | backgroundColor: colors.defaultBackground,
43 | flex: 1
44 | },
45 | itemContainer: {
46 | flexDirection: 'row',
47 | alignItems: 'center',
48 | justifyContent: 'flex-start'
49 | },
50 | itemTitle: {
51 | fontSize: measures.fontSizeMedium,
52 | margin: measures.defaultMargin
53 | }
54 | });
--------------------------------------------------------------------------------
/ios/Partnr-tvOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UIViewControllerBasedStatusBarAppearance
38 |
39 | NSLocationWhenInUseUsageDescription
40 |
41 | NSAppTransportSecurity
42 |
43 |
44 | NSExceptionDomains
45 |
46 | localhost
47 |
48 | NSExceptionAllowsInsecureHTTPLoads
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/partnr/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.partnr;
2 |
3 | import android.app.Application;
4 |
5 | import com.facebook.react.ReactApplication;
6 | import com.oblador.vectoricons.VectorIconsPackage;
7 | import com.horcrux.svg.SvgPackage;
8 | import com.azendoo.reactnativesnackbar.SnackbarPackage;
9 | import br.com.classapp.RNSensitiveInfo.RNSensitiveInfoPackage;
10 | import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
11 | import org.reactnative.camera.RNCameraPackage;
12 | import com.facebook.react.ReactNativeHost;
13 | import com.facebook.react.ReactPackage;
14 | import com.facebook.react.shell.MainReactPackage;
15 | import com.facebook.soloader.SoLoader;
16 |
17 | import java.util.Arrays;
18 | import java.util.List;
19 |
20 | public class MainApplication extends Application implements ReactApplication {
21 |
22 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
23 | @Override
24 | public boolean getUseDeveloperSupport() {
25 | return BuildConfig.DEBUG;
26 | }
27 |
28 | @Override
29 | protected List getPackages() {
30 | return Arrays.asList(
31 | new MainReactPackage(),
32 | new VectorIconsPackage(),
33 | new SvgPackage(),
34 | new SnackbarPackage(),
35 | new RNSensitiveInfoPackage(),
36 | new RNGestureHandlerPackage(),
37 | new RNCameraPackage()
38 | );
39 | }
40 |
41 | @Override
42 | protected String getJSMainModuleName() {
43 | return "index";
44 | }
45 | };
46 |
47 | @Override
48 | public ReactNativeHost getReactNativeHost() {
49 | return mReactNativeHost;
50 | }
51 |
52 | @Override
53 | public void onCreate() {
54 | super.onCreate();
55 | SoLoader.init(this, /* native exopackage */ false);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/common/services/__tests__/api.js:
--------------------------------------------------------------------------------
1 | import * as Api from '../api';
2 |
3 | describe('ApiService', () => {
4 |
5 | it('should be able to get the price conversion between ETH, USD, JMD, and EUR', async function() {
6 | try {
7 | const response = await Api.getPrice();
8 | expect(response.status).toBe(200);
9 | expect(response.data.USD).toBeDefined();
10 | expect(response.data.USD).not.toBeNaN();
11 | expect(response.data.EUR).toBeDefined();
12 | expect(response.data.EUR).not.toBeNaN();
13 | expect(response.data.JMD).toBeDefined();
14 | expect(response.data.JMD).not.toBeNaN();
15 | } catch (e) {
16 | fail(e);
17 | }
18 | });
19 |
20 | it('should be able to get the transaction history for an existing wallet address', async function() {
21 | try {
22 | const walletAddress = '0x589d41feE71B6c972F7AB20e1Fa6EeC11e6C3dF6';
23 | const response = await Api.getHistory(walletAddress);
24 | expect(response.status).toBe(200);
25 | expect(response.data.status).toBe('1');
26 | expect(response.data.result).toBeInstanceOf(Array);
27 | expect(response.data.result.length).toBeGreaterThan(0);
28 | } catch (e) {
29 | fail(e);
30 | }
31 | });
32 |
33 | it('should be able to get an empty transaction history for an unexisting wallet address', async function() {
34 | try {
35 | const walletAddress = 'notavalidaddress';
36 | const response = await Api.getHistory(walletAddress);
37 | expect(response.status).toBe(200);
38 | expect(response.data.status).toBe('0');
39 | expect(response.data.result).toBe('Error! Invalid address format');
40 | } catch (e) {
41 | fail(e);
42 | }
43 | });
44 | });
--------------------------------------------------------------------------------
/src/common/utils/wallet.js:
--------------------------------------------------------------------------------
1 | import ethers from 'ethers';
2 |
3 | const { HDNode, providers, utils, Wallet } = ethers;
4 |
5 | const network = (process.env.NODE_ENV === 'production') ? 'mainnet' : 'rinkeby';
6 | // let network = (process.env.NODE_ENV === 'production') ?
7 | // { name: 'mainnet', ensAddress: '0x314159265dd8dbb310642f98f50c066173c1259b', chainId: 1 } :
8 | // { name: 'rinkeby', ensAddress: '0xe7410170f87102df0055eb195163a03b7f2bff4a', chainId: 4 };
9 |
10 | const PROVIDER = providers.getDefaultProvider(network);
11 |
12 | export function generateMnemonics() {
13 | return HDNode.entropyToMnemonic(utils.randomBytes(16)).split(' ');
14 | }
15 |
16 | export function loadWalletFromMnemonics(mnemonics) {
17 | if (!(mnemonics instanceof Array) && typeof mnemonics !== 'string')
18 | throw new Error('invalid mnemonic');
19 | else if (mnemonics instanceof Array)
20 | mnemonics = mnemonics.join(' ');
21 |
22 | const wallet = Wallet.fromMnemonic(mnemonics);
23 | wallet.provider = PROVIDER;
24 | return wallet;
25 | }
26 |
27 | export function loadWalletFromPrivateKey(pk) {
28 | try {
29 | if (pk.indexOf('0x') !== 0) pk = `0x${pk}`;
30 | return new Wallet(pk, PROVIDER);
31 | } catch (e) {
32 | throw new Error('invalid private key');
33 | }
34 | }
35 |
36 | export function formatBalance(balance) {
37 | return utils.formatEther(balance);
38 | }
39 |
40 | export function reduceBigNumbers(items) {
41 | if (!(items instanceof Array)) throw new Error('The input is not an Array');
42 | return items.reduce((prev, next) => prev.add(next), utils.bigNumberify('0'));
43 | }
44 |
45 | export function calculateFee({ gasUsed, gasPrice }) {
46 | return gasUsed * Number(formatBalance(gasPrice));
47 | }
48 |
49 | export function estimateFee({ gasLimit, gasPrice }) {
50 | return utils.bigNumberify(String(gasLimit)).mul(String(gasPrice));
51 | }
--------------------------------------------------------------------------------
/src/common/actions/__tests__/recents.js:
--------------------------------------------------------------------------------
1 | import * as Action from '../recents';
2 |
3 | describe('RecentsActions', () => {
4 |
5 | const recentsServiceStub = {
6 | loadRecentAddresses: jest.fn(),
7 | saveRecentAddresses: jest.fn()
8 | };
9 |
10 | const recentsStoreStub = {
11 | addAddress: jest.fn(),
12 | isLoading: jest.fn(),
13 | loadAddresses: jest.fn()
14 | };
15 |
16 | beforeEach(() => {
17 | recentsServiceStub.loadRecentAddresses.mockReset();
18 | recentsStoreStub.addAddress.mockReset();
19 | recentsStoreStub.isLoading.mockReset();
20 | recentsStoreStub.loadAddresses.mockReset();
21 | recentsStoreStub.list = [];
22 | });
23 |
24 | it('should update the store while loading recents', async () => {
25 | const address = '0x12345';
26 | recentsServiceStub.loadRecentAddresses.mockImplementationOnce(() => [address]);
27 | await Action.loadRecents(recentsStoreStub, recentsServiceStub);
28 | expect(recentsStoreStub.isLoading).toHaveBeenCalledTimes(2);
29 | expect(recentsStoreStub.isLoading).toHaveBeenNthCalledWith(1, true);
30 | expect(recentsStoreStub.isLoading).toHaveBeenNthCalledWith(2, false);
31 | expect(recentsStoreStub.loadAddresses).toHaveBeenCalledWith([address])
32 | });
33 |
34 | it('should save new address to the recents and storage', async () => {
35 | const address = '0x12345';
36 | await Action.saveAddressToRecents(address, recentsStoreStub, recentsServiceStub);
37 | expect(recentsStoreStub.addAddress).toHaveBeenCalledWith(address);
38 | expect(recentsStoreStub.isLoading).toHaveBeenCalledTimes(2);
39 | expect(recentsStoreStub.isLoading).toHaveBeenNthCalledWith(1, true);
40 | expect(recentsStoreStub.isLoading).toHaveBeenNthCalledWith(2, false);
41 | expect(recentsServiceStub.saveRecentAddresses).toHaveBeenCalled();
42 | });
43 | });
--------------------------------------------------------------------------------
/src/components/views/SelectDestination/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { Button, Camera, InputWithIcon } from '@components/widgets';
4 | import { colors, measures } from '@common/styles';
5 | import Recents from './Recents';
6 |
7 | export class SelectDestination extends React.Component {
8 |
9 | static navigationOptions = { title: 'Select destination' };
10 |
11 | state = { address: '' };
12 |
13 | onPressContinue() {
14 | const { amount } = this.props.navigation.state.params;
15 | const { address } = this.state;
16 | this.props.navigation.navigate('ConfirmTransaction', { address, amount });
17 | }
18 |
19 | render() {
20 | return (
21 |
22 | this.setState({ address })}
28 | onPressIcon={() => this.refs.camera.show()} />
29 |
30 | this.refs.input.onChangeText(address)} />
31 |
32 | this.onPressContinue()} />
33 | this.refs.camera.hide()}
37 | onBarCodeRead={address => this.refs.input.onChangeText(address)} />
38 |
39 | );
40 | }
41 | }
42 |
43 | const styles = StyleSheet.create({
44 | container: {
45 | backgroundColor: colors.white,
46 | flex: 1,
47 | padding: measures.defaultPadding
48 | },
49 | content: {
50 | flex: 1,
51 | alignItems: 'stretch',
52 | marginVertical: measures.defaultMargin
53 | }
54 | });
--------------------------------------------------------------------------------
/src/components/views/WalletExtract/Balance.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { colors, measures } from '@common/styles';
5 | import { Wallet as WalletUtils } from '@common/utils';
6 |
7 | @inject('prices', 'wallet')
8 | @observer
9 | export default class Balance extends React.Component {
10 |
11 | get balance() {
12 | const { item } = this.props.wallet;
13 | return Number(WalletUtils.formatBalance(item.balance));
14 | }
15 |
16 | get fiatBalance() {
17 | return Number(this.props.prices.usd * this.balance);
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
24 | Balance:
25 |
26 |
27 | ETH {this.balance.toFixed(3)}
28 | US$ {this.fiatBalance.toFixed(2)}
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | container: {
37 | alignItems: 'center',
38 | justifyContent: 'flex-start',
39 | height: 60,
40 | flexDirection: 'row',
41 | borderBottomWidth: 1,
42 | borderBottomColor: colors.lightGray
43 | },
44 | leftColumn: {
45 | flex: 1
46 | },
47 | title: {
48 | fontSize: measures.fontSizeLarge,
49 | color: colors.gray
50 | },
51 | balance: {
52 | fontSize: measures.fontSizeMedium + 2,
53 | fontWeight: 'bold',
54 | color: colors.gray
55 | },
56 | fiatBalance: {
57 | fontSize: measures.fontSizeMedium - 3,
58 | color: colors.gray
59 | },
60 | rightColumn: {
61 | flex: 1,
62 | alignItems: 'flex-end',
63 | justifyContent: 'center'
64 | }
65 | });
--------------------------------------------------------------------------------
/src/components/views/WalletExtract/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FlatList, RefreshControl, StyleSheet, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { measures } from '@common/styles';
5 | import { Wallets as WalletActions } from '@common/actions';
6 | import Balance from './Balance';
7 | import TransactionCard from './TransactionCard';
8 | import NoTransactions from './NoTransactions';
9 | import { GeneralActions } from '@common/actions';
10 |
11 | @inject('wallet')
12 | @observer
13 | export class WalletExtract extends React.Component {
14 |
15 | componentDidMount() {
16 | this.updateHistory();
17 | }
18 |
19 | async updateHistory() {
20 | try {
21 | await WalletActions.updateHistory(this.props.wallet.item);
22 | } catch (e) {
23 | GeneralActions.notify(e.message, 'long');
24 | }
25 | }
26 |
27 | renderItem = (address) => ({ item }) =>
28 |
29 | renderBody = ({ item, history, loading, pendingTransactions }) => (!history.length && !loading) ? : (
30 | this.updateHistory()} />}
34 | keyExtractor={(element) => element.hash}
35 | renderItem={this.renderItem(item.getAddress())} />
36 | );
37 |
38 | render() {
39 | return (
40 |
41 |
42 | {this.renderBody(this.props.wallet)}
43 |
44 | );
45 | }
46 | }
47 |
48 | const styles = StyleSheet.create({
49 | container: {
50 | alignItems: 'stretch',
51 | justifyContent: 'flex-start',
52 | flex: 1,
53 | padding: measures.defaultPadding
54 | },
55 | content: {
56 | marginTop: measures.defaultMargin
57 | }
58 | });
--------------------------------------------------------------------------------
/src/components/views/NewWallet/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { Button } from '@components/widgets';
4 | import { colors, measures } from '@common/styles';
5 |
6 | export class NewWallet extends React.Component {
7 |
8 | static navigationOptions = { title: 'New Wallet' };
9 |
10 | onPressLoad() {
11 | const { walletName, walletDescription } = this.props.navigation.state.params;
12 | this.props.navigation.navigate('LoadWallet', { walletName, walletDescription });
13 | }
14 |
15 | onPressCreate() {
16 | const { walletName, walletDescription } = this.props.navigation.state.params;
17 | this.props.navigation.navigate('CreateWallet', { walletName, walletDescription });
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
24 | Do you already have a wallet to configure?
25 |
26 |
27 | this.onPressLoad()}>Yes, load it
28 | this.onPressCreate()}>No, create new
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | container: {
37 | backgroundColor: colors.defaultBackground,
38 | alignItems: 'stretch',
39 | justifyContent: 'space-between',
40 | flex: 1,
41 | padding: measures.defaultPadding,
42 | },
43 | contentContainer: {
44 | flex: 1,
45 | justifyContent: 'space-around'
46 | },
47 | message: {
48 | color: colors.black,
49 | fontSize: 16,
50 | textAlign: 'center',
51 | marginVertical: measures.defaultMargin,
52 | marginHorizontal: 32
53 | },
54 | buttonsContainer: {
55 | justifyContent: 'space-between'
56 | }
57 | });
--------------------------------------------------------------------------------
/src/common/stores/__tests__/recents.js:
--------------------------------------------------------------------------------
1 | import { RecentsStore } from '../recents';
2 |
3 | describe('RecentsStore', () => {
4 |
5 | let recentsStore;
6 |
7 | beforeEach(() => recentsStore = new RecentsStore());
8 |
9 | it('should be able to change the loading state', () => {
10 | recentsStore.isLoading(true);
11 | expect(recentsStore.loading).toBe(true);
12 | });
13 |
14 | it('should be able to add a new address to the store list', () => {
15 | expect(recentsStore.list.length).toBe(0);
16 | recentsStore.addAddress('0x12345');
17 | expect(recentsStore.list.length).toBe(1);
18 | expect(recentsStore.list[0]).toBe('0x12345');
19 | });
20 |
21 | it('should be filter repeated addresses', () => {
22 | expect(recentsStore.list.length).toBe(0);
23 | recentsStore.addAddress('0x12345');
24 | expect(recentsStore.list.length).toBe(1);
25 | recentsStore.addAddress('0x12345');
26 | expect(recentsStore.list.length).toBe(1);
27 | });
28 |
29 | it('should be able to update the state with a list of addresses', () => {
30 | expect(recentsStore.list.length).toBe(0);
31 | recentsStore.loadAddresses(['0x12345', '0x54321']);
32 | expect(recentsStore.list.length).toBe(2);
33 | });
34 |
35 | it('should avoid adding duplicates when updating the state with a list of addresses', () => {
36 | expect(recentsStore.list.length).toBe(0);
37 | recentsStore.loadAddresses(['0x12345', '0x54321', '0x12345']);
38 | expect(recentsStore.list.length).toBe(2);
39 | });
40 |
41 | it('should be able to reset the store state', () => {
42 | recentsStore.isLoading(true);
43 | recentsStore.addAddress('0x12345');
44 | recentsStore.reset();
45 | expect(recentsStore.list.length).toBe(0);
46 | expect(recentsStore.loading).toBe(false);
47 | });
48 | });
--------------------------------------------------------------------------------
/src/components/widgets/InputWithIcon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
3 | import { Icon } from '@components/widgets';
4 | import { colors } from '@common/styles';
5 |
6 | export class InputWithIcon extends React.Component {
7 |
8 | state = { text: '' };
9 |
10 | onChangeText(text) {
11 | const { onChangeText } = this.props;
12 | this.setState({ text });
13 | if (onChangeText) onChangeText(text);
14 | }
15 |
16 | onPressIcon() {
17 | let { text } = this.state;
18 | text = text.trim();
19 | this.props.onPressIcon(text);
20 | }
21 |
22 | render() {
23 | const { autoFocus, icon, placeholder } = this.props;
24 | return (
25 |
26 | this.onChangeText(text)}
33 | underlineColorAndroid="transparent"
34 | placeholder={placeholder}
35 | placeholderTextColor={colors.black} />
36 | this.onPressIcon()}>
37 |
41 |
42 |
43 | );
44 | }
45 | }
46 |
47 | const styles = StyleSheet.create({
48 | container: {
49 | flexDirection: 'row',
50 | alignItems: 'center',
51 | justifyContent: 'space-between'
52 | },
53 | input: {
54 | width: '90%',
55 | borderBottomWidth: 1,
56 | borderBottomColor: colors.black,
57 | padding: 4,
58 | paddingLeft: 0,
59 | marginRight: 2,
60 | textAlign: 'center',
61 | color: colors.black
62 | }
63 | });
--------------------------------------------------------------------------------
/src/folderStructure.txt:
--------------------------------------------------------------------------------
1 | .
2 | ├── Router.js
3 | ├── assets
4 | │ └── ethereum-logo.png
5 | ├── common
6 | │ ├── actions
7 | │ │ ├── __tests__
8 | │ │ ├── general.js
9 | │ │ ├── index.js
10 | │ │ ├── prices.js
11 | │ │ ├── recents.js
12 | │ │ ├── transactions.js
13 | │ │ └── wallets.js
14 | │ ├── constants
15 | │ │ ├── index.js
16 | │ │ ├── recents.js
17 | │ │ ├── storage.js
18 | │ │ ├── url.js
19 | │ │ └── wallet.js
20 | │ ├── services
21 | │ │ ├── __tests__
22 | │ │ ├── api.js
23 | │ │ ├── index.js
24 | │ │ ├── recents.js
25 | │ │ ├── storage.js
26 | │ │ ├── transactions.js
27 | │ │ └── wallets.js
28 | │ ├── stores
29 | │ │ ├── __tests__
30 | │ │ ├── index.js
31 | │ │ ├── prices.js
32 | │ │ ├── recents.js
33 | │ │ ├── wallet.js
34 | │ │ └── wallets.js
35 | │ ├── styles
36 | │ │ ├── colors.js
37 | │ │ ├── index.js
38 | │ │ └── measures.js
39 | │ └── utils
40 | │ ├── __tests__
41 | │ ├── image.js
42 | │ ├── index.js
43 | │ ├── shims.js
44 | │ ├── transaction.js
45 | │ └── wallet.js
46 | ├── components
47 | │ ├── views
48 | │ │ ├── ChangeCurrency
49 | │ │ ├── ConfirmMnemonics
50 | │ │ ├── ConfirmTransaction
51 | │ │ ├── CreateMnemonics
52 | │ │ ├── CreateWallet
53 | │ │ ├── LoadMnemonics
54 | │ │ ├── LoadPrivateKey
55 | │ │ ├── LoadWallet
56 | │ │ ├── NewWallet
57 | │ │ ├── NewWalletName
58 | │ │ ├── ReceiveCoins
59 | │ │ ├── SelectDestination
60 | │ │ ├── SendCoins
61 | │ │ ├── Settings
62 | │ │ ├── ShowPrivateKey
63 | │ │ ├── WalletDetails
64 | │ │ ├── WalletExtract
65 | │ │ ├── WalletSettings
66 | │ │ ├── WalletsOverview
67 | │ │ └── index.js
68 | │ └── widgets
69 | │ ├── Button
70 | │ ├── Calculator
71 | │ ├── Camera
72 | │ ├── HeaderIcon
73 | │ ├── Icon
74 | │ ├── InputWithIcon
75 | │ ├── NumberGrid
76 | │ ├── TabView
77 | │ ├── TextBullet
78 | │ └── index.js
79 | ├── index.js
80 | ├── partnr-tree.txt
81 | └── setupTests.js
82 |
83 | 43 directories, 37 files
--------------------------------------------------------------------------------
/ios/PartnrTests/PartnrTests.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | #import
9 | #import
10 |
11 | #import
12 | #import
13 |
14 | #define TIMEOUT_SECONDS 600
15 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!"
16 |
17 | @interface PartnrTests : XCTestCase
18 |
19 | @end
20 |
21 | @implementation PartnrTests
22 |
23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test
24 | {
25 | if (test(view)) {
26 | return YES;
27 | }
28 | for (UIView *subview in [view subviews]) {
29 | if ([self findSubviewInView:subview matching:test]) {
30 | return YES;
31 | }
32 | }
33 | return NO;
34 | }
35 |
36 | - (void)testRendersWelcomeScreen
37 | {
38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
40 | BOOL foundElement = NO;
41 |
42 | __block NSString *redboxError = nil;
43 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
44 | if (level >= RCTLogLevelError) {
45 | redboxError = message;
46 | }
47 | });
48 |
49 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
50 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
51 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
52 |
53 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
54 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
55 | return YES;
56 | }
57 | return NO;
58 | }];
59 | }
60 |
61 | RCTSetLogFunction(RCTDefaultLogFunction);
62 |
63 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
64 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
65 | }
66 |
67 |
68 | @end
69 |
--------------------------------------------------------------------------------
/src/components/views/CreateWallet/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { Button } from '@components/widgets';
4 | import { colors, measures } from '@common/styles';
5 |
6 | export class CreateWallet extends React.Component {
7 |
8 | static navigationOptions = { title: 'Create Wallet' };
9 |
10 | onPressProceed() {
11 | const { walletName, walletDescription } = this.props.navigation.state.params;
12 | this.props.navigation.navigate('CreateMnemonics', { walletName, walletDescription });
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 | When creating a new wallet you will receive a sequence of mnemonics which represent your "personal password". Anyone with this sequence may be able to reconfigure your wallet in any new device. Keep it stored as secure as possible. Only you should have access to this information.
21 | Write it somewhere safe so you can make sure you won't lose it, or you may lose permanently all your coins. There is no way to recover it later.
22 |
23 |
24 |
25 | this.onPressProceed()}>Proceed
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | const styles = StyleSheet.create({
33 | container: {
34 | backgroundColor: colors.defaultBackground,
35 | alignItems: 'stretch',
36 | justifyContent: 'space-between',
37 | flex: 1,
38 | padding: measures.defaultPadding,
39 | },
40 | contentContainer: {
41 | flex: 1,
42 | justifyContent: 'space-around'
43 | },
44 | message: {
45 | color: colors.black,
46 | fontSize: 16,
47 | textAlign: 'center',
48 | marginVertical: measures.defaultMargin,
49 | marginHorizontal: 32
50 | },
51 | buttonsContainer: {
52 | justifyContent: 'space-between'
53 | }
54 | });
--------------------------------------------------------------------------------
/src/components/views/WalletsOverview/TotalBalance.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { colors, measures } from '@common/styles';
5 | import { Wallet as WalletUtils } from '@common/utils';
6 |
7 | @inject('prices')
8 | @observer
9 |
10 | export default class TotalBalance extends React.Component {
11 |
12 | get balance() {
13 | const { wallets } = this.props;
14 | const balances = wallets.map(({ balance }) => balance);
15 | if (balances.some(el => !el)) return 0;
16 | const balance = WalletUtils.reduceBigNumbers(balances).toString();
17 | return Number(WalletUtils.formatBalance(balance));
18 | }
19 |
20 | get fiatBalance() {
21 | return Number(this.props.prices.usd * this.balance);
22 | }
23 |
24 | render() {
25 | return (
26 |
27 |
28 | Total balance:
29 |
30 |
31 | ETH {this.balance.toFixed(3)}
32 | US$ {this.fiatBalance.toFixed(2)}
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | const styles = StyleSheet.create({
40 | container: {
41 | alignItems: 'center',
42 | justifyContent: 'flex-start',
43 | height: 60,
44 | flexDirection: 'row',
45 | borderBottomWidth: 1,
46 | borderBottomColor: colors.lightGray
47 | },
48 | leftColumn: {
49 | flex: 1
50 | },
51 | title: {
52 | fontSize: measures.fontSizeLarge,
53 | color: colors.gray
54 | },
55 | balance: {
56 | fontSize: measures.fontSizeMedium + 2,
57 | fontWeight: 'bold',
58 | color: colors.gray
59 | },
60 | fiatBalance: {
61 | fontSize: measures.fontSizeMedium - 3,
62 | color: colors.gray
63 | },
64 | rightColumn: {
65 | flex: 1,
66 | alignItems: 'flex-end',
67 | justifyContent: 'center'
68 | }
69 | });
--------------------------------------------------------------------------------
/src/components/widgets/Calculator/Panel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { colors, measures } from '@common/styles';
5 |
6 | @inject('prices')
7 | @observer
8 | export default class Panel extends React.Component {
9 |
10 | state = { amount: '' };
11 |
12 | get amount() {
13 | return this.state.amount || 0;
14 | }
15 |
16 | get fiatAmount() {
17 | return (this.amount * this.props.prices.usd).toFixed(2);
18 | }
19 |
20 | onChange(value) {
21 | let { amount } = this.state;
22 | switch (value) {
23 | case 'erase':
24 | amount = amount.slice(0, amount.length-1);
25 | break;
26 |
27 | case '.':
28 | if (amount.indexOf('.') > -1) return;
29 | else if (!amount.length) amount += '0.';
30 | else amount += '.';
31 | break;
32 |
33 | default:
34 | if (amount === '0') amount = value;
35 | else amount += value;
36 | break;
37 | }
38 | this.setState({ amount });
39 | }
40 |
41 | render() {
42 | return (
43 |
44 |
45 | {this.amount}
46 | ETH
47 |
48 |
49 | US$ {this.fiatAmount}
50 |
51 |
52 | );
53 | }
54 | }
55 |
56 | const styles = StyleSheet.create({
57 | container: {
58 | flex: 1,
59 | alignItems: 'center',
60 | justifyContent: 'space-around'
61 | },
62 | row: {
63 | alignItems: 'center',
64 | justifyContent: 'center',
65 | flexDirection: 'row'
66 | },
67 | amount: {
68 | fontSize: measures.fontSizeLarge,
69 | fontWeight: 'bold'
70 | },
71 | unit: {
72 | fontSize: measures.fontSizeMedium,
73 | color: colors.gray,
74 | marginLeft: measures.defaultMargin
75 | }
76 | });
--------------------------------------------------------------------------------
/contracts/Votes.sol:
--------------------------------------------------------------------------------
1 | pragma solidity >=0.4.22 < 0.6.0;
2 |
3 | /// @title Voting with delegation.
4 | contract Ballot {
5 | // This declares a new complex type which will
6 | // be used for variables later.
7 | // It will represent a single voter.
8 | struct Voter {
9 | bool voted; // if true, that person already voted
10 | uint vote; // index of the voted proposal
11 | }
12 |
13 | // This is a type for a single proposal.
14 | struct Proposal {
15 | bytes32 name; // short name (up to 32 bytes)
16 | uint voteCount; // number of accumulated votes
17 | }
18 |
19 | // This declares a state variable that
20 | // stores a `Voter` struct for each possible address.
21 | mapping(address => Voter) public voters;
22 |
23 | // A dynamically-sized array of `Proposal` structs.
24 | Proposal[] public proposals;
25 |
26 |
27 | /// Give your vote (including votes delegated to you)
28 | /// to proposal `proposals[proposal].name`.
29 | function vote(uint proposal) public {
30 | Voter storage sender = voters[msg.sender];
31 | require(
32 | !sender.voted,
33 | "Already voted."
34 | );
35 | sender.voted = true;
36 | sender.vote = proposal;
37 |
38 | // If `proposal` is out of the range of the array,
39 | // this will throw automatically and revert all
40 | // changes.
41 | proposals[proposal].voteCount ++;
42 | }
43 |
44 | /// @dev Computes the winning proposal taking all
45 | /// previous votes into account.
46 | function winningProposal() public view
47 | returns (uint winningProposal_)
48 | {
49 | uint winningVoteCount = 0;
50 | for (uint p = 0; p < proposals.length; p++) {
51 | if (proposals[p].voteCount > winningVoteCount) {
52 | winningVoteCount = proposals[p].voteCount;
53 | winningProposal_ = p;
54 | }
55 | }
56 | }
57 |
58 | // Calls winningProposal() function to get the index
59 | // of the winner contained in the proposals array and then
60 | // returns the name of the winner
61 | function winnerName() public view returns (bytes32 winnerName_) {
62 | winnerName_ = proposals[winningProposal()].name;
63 | }
64 | }
--------------------------------------------------------------------------------
/src/Router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | createAppContainer,
4 | NavigationActions,
5 | createStackNavigator
6 | } from 'react-navigation';
7 | import * as Views from './components/views';
8 | import { colors } from './common/styles';
9 |
10 | export const INITIAL_ROUTE = 'WalletsOverview';
11 |
12 | const navigator = createStackNavigator(
13 | {
14 | ChangeCurrency: { screen: Views.ChangeCurrency },
15 | ConfirmMnemonics: { screen: Views.ConfirmMnemonics },
16 | ConfirmTransaction: { screen: Views.ConfirmTransaction },
17 | CreateMnemonics: { screen: Views.CreateMnemonics },
18 | CreateWallet: { screen: Views.CreateWallet },
19 | LoadMnemonics: { screen: Views.LoadMnemonics },
20 | LoadPrivateKey: { screen: Views.LoadPrivateKey },
21 | LoadWallet: { screen: Views.LoadWallet },
22 | NewWallet: { screen: Views.NewWallet },
23 | NewWalletName: { screen: Views.NewWalletName },
24 | SelectDestination: { screen: Views.SelectDestination },
25 | Settings: { screen: Views.Settings },
26 | ShowPrivateKey: { screen: Views.ShowPrivateKey },
27 | WalletDetails: { screen: Views.WalletDetails },
28 | WalletsOverview: { screen: Views.WalletsOverview }
29 | },
30 | {
31 | initialRouteName: INITIAL_ROUTE,
32 | defaultNavigationOptions: {
33 | headerStyle: {
34 | backgroundColor: colors.primary
35 | },
36 | headerTintColor: colors.secondary,
37 | tintColor: colors.secondary
38 | }
39 | }
40 | );
41 |
42 | const parentGetStateForAction = navigator.router.getStateForAction;
43 |
44 | navigator.router.getStateForAction = (action, inputState) => {
45 | const state = parentGetStateForAction(action, inputState);
46 |
47 | // fix it up if applicable
48 | if (state && action.type === NavigationActions.NAVIGATE) {
49 | if (action.params && action.params.replaceRoute) {
50 | const leave = action.params.leave || 1;
51 | delete action.params.replaceRoute;
52 | while (state.routes.length > leave && state.index > 0) {
53 | const oldIndex = state.index - 1;
54 | // remove one that we are replacing
55 | state.routes.splice(oldIndex, 1);
56 | // index now one less
57 | state.index = oldIndex;
58 | }
59 | }
60 | }
61 |
62 | return state;
63 | };
64 |
65 | const AppContainer = createAppContainer(navigator);
66 |
67 | export default AppContainer;
68 |
--------------------------------------------------------------------------------
/src/components/widgets/NumberGrid/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
3 | import { Icon } from '@components/widgets';
4 | import { colors, measures } from '@common/styles';
5 |
6 | export class NumberGrid extends React.Component {
7 |
8 | renderBlock = (label, value) => (
9 | this.props.onPressNumber(value)}>
10 |
11 | {label}
12 |
13 |
14 | );
15 |
16 | render() {
17 | return (
18 |
19 |
20 | {this.renderBlock('9', '9')}
21 | {this.renderBlock('8', '8')}
22 | {this.renderBlock('7', '7')}
23 |
24 |
25 | {this.renderBlock('6', '6')}
26 | {this.renderBlock('5', '5')}
27 | {this.renderBlock('4', '4')}
28 |
29 |
30 | {this.renderBlock('3', '3')}
31 | {this.renderBlock('2', '2')}
32 | {this.renderBlock('1', '1')}
33 |
34 |
35 | {this.renderBlock(, '.')}
36 | {this.renderBlock('0', '0')}
37 | {this.renderBlock(, 'erase')}
38 |
39 |
40 | );
41 | }
42 | }
43 |
44 | const styles = StyleSheet.create({
45 | container: {
46 | flex: 4,
47 | alignItems: 'stretch',
48 | justifyContent: 'center',
49 | flexDirection: 'column'
50 | },
51 | row: {
52 | flex: 1,
53 | flexDirection: 'row',
54 | alignItems: 'stretch',
55 | justifyContent: 'center'
56 | },
57 | block: {
58 | flex: 1,
59 | borderWidth: 2,
60 | borderColor: colors.secondary,
61 | backgroundColor: colors.lightGray,
62 | alignItems: 'center',
63 | justifyContent: 'center'
64 | },
65 | label: {
66 | fontSize: measures.fontSizeMedium,
67 | fontWeight: 'bold'
68 | }
69 | });
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | ; We fork some components by platform
3 | .*/*[.]android.js
4 |
5 | ; Ignore "BUCK" generated dirs
6 | /\.buckd/
7 |
8 | ; Ignore unexpected extra "@providesModule"
9 | .*/node_modules/.*/node_modules/fbjs/.*
10 |
11 | ; Ignore duplicate module providers
12 | ; For RN Apps installed via npm, "Libraries" folder is inside
13 | ; "node_modules/react-native" but in the source repo it is in the root
14 | .*/Libraries/react-native/React.js
15 |
16 | ; Ignore polyfills
17 | .*/Libraries/polyfills/.*
18 |
19 | ; Ignore metro
20 | .*/node_modules/metro/.*
21 |
22 | [include]
23 |
24 | [libs]
25 | node_modules/react-native/Libraries/react-native/react-native-interface.js
26 | node_modules/react-native/flow/
27 | node_modules/react-native/flow-github/
28 |
29 | [options]
30 | emoji=true
31 |
32 | module.system=haste
33 | module.system.haste.use_name_reducers=true
34 | # get basename
35 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
36 | # strip .js or .js.flow suffix
37 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
38 | # strip .ios suffix
39 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
40 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
41 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
42 | module.system.haste.paths.blacklist=.*/__tests__/.*
43 | module.system.haste.paths.blacklist=.*/__mocks__/.*
44 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.*
45 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.*
46 |
47 | munge_underscores=true
48 |
49 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
50 |
51 | module.file_ext=.js
52 | module.file_ext=.jsx
53 | module.file_ext=.json
54 | module.file_ext=.native.js
55 |
56 | suppress_type=$FlowIssue
57 | suppress_type=$FlowFixMe
58 | suppress_type=$FlowFixMeProps
59 | suppress_type=$FlowFixMeState
60 |
61 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
62 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
63 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
64 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
65 |
66 | [version]
67 | ^0.98.1
68 |
--------------------------------------------------------------------------------
/ios/Partnr/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Partnr
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | NSLocationWhenInUseUsageDescription
28 |
29 | UILaunchStoryboardName
30 | LaunchScreen
31 | UIRequiredDeviceCapabilities
32 |
33 | armv7
34 |
35 | UISupportedInterfaceOrientations
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationLandscapeLeft
39 | UIInterfaceOrientationLandscapeRight
40 |
41 | UIViewControllerBasedStatusBarAppearance
42 |
43 | NSAppTransportSecurity
44 |
45 | NSAllowsArbitraryLoads
46 |
47 | NSExceptionDomains
48 |
49 | localhost
50 |
51 | NSExceptionAllowsInsecureHTTPLoads
52 |
53 |
54 |
55 |
56 | UIAppFonts
57 |
58 | AntDesign.ttf
59 | Entypo.ttf
60 | EvilIcons.ttf
61 | Feather.ttf
62 | FontAwesome.ttf
63 | FontAwesome5_Brands.ttf
64 | FontAwesome5_Regular.ttf
65 | FontAwesome5_Solid.ttf
66 | Foundation.ttf
67 | Ionicons.ttf
68 | MaterialCommunityIcons.ttf
69 | MaterialIcons.ttf
70 | Octicons.ttf
71 | SimpleLineIcons.ttf
72 | Zocial.ttf
73 | Fontisto.ttf
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/components/widgets/Icon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Platform } from 'react-native';
3 | import Entypo from 'react-native-vector-icons/dist/Entypo';
4 | import EvilIcon from 'react-native-vector-icons/dist/EvilIcons';
5 | import Feather from 'react-native-vector-icons/dist/Feather';
6 | import FontAwesome from 'react-native-vector-icons/dist/FontAwesome';
7 | import Foundation from 'react-native-vector-icons/dist/Foundation';
8 | import Ionicon from 'react-native-vector-icons/dist/Ionicons';
9 | import MaterialIcon from 'react-native-vector-icons/dist/MaterialIcons';
10 | import MaterialCommunityIcon from 'react-native-vector-icons/dist/MaterialCommunityIcons';
11 | import Octicon from 'react-native-vector-icons/dist/Octicons';
12 | import Zocial from 'react-native-vector-icons/dist/Zocial';
13 | import SimpleLineIcon from 'react-native-vector-icons/SimpleLineIcons';
14 | import { colors, measures } from '@common/styles';
15 |
16 | function getSize(size) {
17 | if ((size >>> 0) > 0) return size;
18 | switch (size) {
19 | case 'small': return measures.iconSizeSmall;
20 | case 'large': return measures.iconSizeLarge;
21 | default:
22 | case 'medium': return measures.iconSizeMedium;
23 | }
24 | }
25 |
26 | function getIonicon({ name, size, ...props }) {
27 | name = (Platform.OS === 'ios') ? `ios-${name}` : `md-${name}`;
28 | return ;
29 | }
30 |
31 | export const Icon = (props) => {
32 | if (!props.name) return null;
33 | const size = getSize(props.size);
34 | const color = props.color || colors.black;
35 | switch (props.type) {
36 | case 'ent': return ;
37 | case 'ei': return ;
38 | case 'fe': return ;
39 | case 'fa': return ;
40 | case 'fo': return ;
41 | case 'md': return ;
42 | case 'mdc': return ;
43 | case 'oct': return ;
44 | case 'zo': return ;
45 | case 'simple': return ;
46 |
47 | default:
48 | case 'ionicons': return getIonicon({ ...props, color, size });
49 | }
50 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Partnr",
3 | "version": "0.0.1",
4 | "license": "MIT",
5 | "scripts": {
6 | "start": "node node_modules/react-native/local-cli/cli.js start",
7 | "test": "jest",
8 | "test:watch": "jest --watch",
9 | "android:build": "cd android && ./gradlew assembleDebug && cd .. && cp android/app/build/outputs/apk/debug/app-debug.apk EthereumWallet.apk",
10 | "android:bundle": "react-native bundle --dev false --platform android --entry-file index.js --bundle-output ./android/app/build/intermediates/assets/debug/index.android.bundle --assets-dest ./android/app/build/intermediates/res/merged/debug",
11 | "android:clean": "cd android && ./gradlew clean && cd ..",
12 | "android:generate-apk": "npm run android:bundle && npm run android:build",
13 | "android": "react-native run-android",
14 | "logcat": "adb logcat *:S ReactNative:V ReactNativeJS:V",
15 | "ios": "react-native run-ios",
16 | "postinstall": "rndebugger-open"
17 | },
18 | "dependencies": {
19 | "axios": "0.28.0",
20 | "ethers": "6.13.2",
21 | "identicon.js": "2.3.3",
22 | "mobx": "5.9.4",
23 | "mobx-react": "5.4.4",
24 | "moment": "2.29.4",
25 | "native-base": "^2.13.8",
26 | "react": "16.8.6",
27 | "react-native": "0.71.4",
28 | "react-native-camera": "2.10.1",
29 | "react-native-easy-grid": "^0.2.2",
30 | "react-native-gesture-handler": "1.2.1",
31 | "react-native-modal": "11.0.1",
32 | "react-native-permissions": "1.1.1",
33 | "react-native-qrcode-svg": "5.1.2",
34 | "react-native-sensitive-info": "5.4.1",
35 | "react-native-snackbar": "1.0.0",
36 | "react-native-svg": "9.4.0",
37 | "react-native-vector-icons": "^6.6.0",
38 | "react-navigation": "3.11.0"
39 | },
40 | "devDependencies": {
41 | "@babel/core": "7.4.5",
42 | "@babel/plugin-proposal-decorators": "7.4.4",
43 | "@babel/plugin-transform-runtime": "7.4.4",
44 | "@babel/runtime": "7.4.5",
45 | "babel-core": "7.0.0-bridge.0",
46 | "babel-jest": "29.7.0",
47 | "babel-plugin-module-resolver": "5.0.0",
48 | "babel-preset-react-native": "4.0.1",
49 | "enzyme": "3.9.0",
50 | "enzyme-adapter-react-16": "1.13.1",
51 | "eslint-config-rallycoding": "3.2.0",
52 | "jest": "29.5.0",
53 | "metro-react-native-babel-preset": "0.54.1",
54 | "react-native-debugger-open": "^0.3.22",
55 | "react-native-mock-render": "0.1.3",
56 | "react-test-renderer": "16.8.6"
57 | },
58 | "jest": {
59 | "preset": "react-native",
60 | "setupFilesAfterEnv": [
61 | "/src/setupTests.js"
62 | ]
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/views/ConfirmMnemonics/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { Button } from '@components/widgets';
4 | import { Wallet as WalletUtils } from '@common/utils';
5 | import { colors, measures } from '@common/styles';
6 | import { General as GeneralActions, Wallets as WalletsActions } from '@common/actions';
7 | import ConfirmBox from './ConfirmBox';
8 |
9 | export class ConfirmMnemonics extends React.Component {
10 |
11 | static navigationOptions = { title: 'Create Wallet' };
12 |
13 | state = { mnemonics: [] };
14 |
15 | componentDidMount() {
16 | const { mnemonics, walletName, walletDescription } = this.props.navigation.state.params;
17 | this.setState({ mnemonics, walletName, walletDescription });
18 | }
19 |
20 | async onPressConfirm() {
21 | if (!this.refs.confirm.isValidSequence()) return;
22 | try {
23 | const { mnemonics, walletName, walletDescription } = this.state;
24 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
25 | await WalletsActions.addWallet(walletName, wallet, walletDescription);
26 | this.props.navigation.navigate('WalletsOverview', { replaceRoute: true });
27 | await WalletsActions.saveWallets();
28 | } catch (e) {
29 | GeneralActions.notify(e.message, 'long');
30 | }
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
37 |
38 |
39 | this.onPressConfirm()}>Confirm & open wallet
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | const styles = StyleSheet.create({
47 | container: {
48 | alignItems: 'center',
49 | justifyContent: 'space-between',
50 | flex: 1,
51 | backgroundColor: colors.defaultBackground
52 | },
53 | message: {
54 | color: colors.black,
55 | fontSize: 16,
56 | textAlign: 'center',
57 | marginVertical: measures.defaultMargin,
58 | marginHorizontal: 32
59 | },
60 | mnemonicsContainer: {
61 | flexDirection: 'row',
62 | justifyContent: 'center',
63 | flexWrap: 'wrap',
64 | maxWidth: '80%'
65 | },
66 | mnemonic: {
67 | margin: 4
68 | },
69 | buttonsContainer: {
70 | width: '100%',
71 | justifyContent: 'flex-end',
72 | height: 104
73 | }
74 | });
75 |
--------------------------------------------------------------------------------
/src/components/views/Settings/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { Icon } from '@components/widgets';
5 | import { colors, measures } from '@common/styles';
6 | import { General as GeneralActions } from '@common/actions';
7 | import ListItem from './ListItem';
8 |
9 | @inject('wallet')
10 | @observer
11 | export class Settings extends React.Component {
12 |
13 | static navigationOptions = ({ navigation, screenProps }) => ({
14 | title: 'Settings'
15 | });
16 |
17 | goToChangeCurrencyPage() {
18 | this.props.navigation.navigate('ChangeCurrency');
19 | }
20 |
21 | eraseAllData() {
22 | GeneralActions.eraseAllData();
23 | this.props.navigation.pop();
24 | }
25 |
26 | confirmErase() {
27 | Alert.alert(
28 | 'Erase all data',
29 | 'This action cannot be undone. Are you sure?',
30 | [
31 | { text: 'Cancel', onPress: () => {}, style: 'cancel' },
32 | { text: 'Erase', onPress: () => this.eraseAllData() }
33 | ],
34 | { cancelable: false }
35 | );
36 | }
37 |
38 | renderItems = (items) => items.map((item, index) => (
39 |
40 |
41 |
42 |
43 |
44 | {item.title}
45 |
46 |
47 | ));
48 |
49 | render() {
50 | return (
51 |
52 | {this.renderItems([
53 | { title: 'Change currency', iconName: 'attach-money', iconType: 'md', action: () => this.goToChangeCurrencyPage() },
54 | { title: 'Erase all data', iconName: 'trash', iconType: '', action: () => this.confirmErase() },
55 | ])}
56 |
57 | );
58 | }
59 | }
60 |
61 | const styles = StyleSheet.create({
62 | container: {
63 | backgroundColor: colors.defaultBackground,
64 | flex: 1
65 | },
66 | itemContainer: {
67 | flexDirection: 'row',
68 | alignItems: 'center',
69 | justifyContent: 'flex-start'
70 | },
71 | icon: {
72 | width: 24,
73 | height: 24,
74 | margin: measures.defaultMargin
75 | },
76 | itemTitle: {
77 | fontSize: measures.fontSizeMedium
78 | }
79 | });
--------------------------------------------------------------------------------
/src/components/views/SelectDestination/Recents.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ActivityIndicator, FlatList, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
3 | import { colors, measures } from '@common/styles';
4 | import { inject, observer } from 'mobx-react';
5 | import { General as GeneralActions, Recents as RecentsActions } from '@common/actions';
6 | import NoRecents from './NoRecents';
7 |
8 | @inject('recents')
9 | @observer
10 | export default class Recents extends React.Component {
11 |
12 | componentDidMount() {
13 | this.loadRecents();
14 | }
15 |
16 | async loadRecents() {
17 | try {
18 | await RecentsActions.loadRecents();
19 | } catch (e) {
20 | GeneralActions.notify(e.message, 'long');
21 | }
22 | }
23 |
24 | renderRecent = ({ item }) => (
25 | this.props.onPressItem(item)}>
26 |
27 | {item}
28 |
29 |
30 | );
31 |
32 | renderList = (recents) => (!recents.length) ? : (
33 | item}
37 | renderItem={this.renderRecent} />
38 | );
39 |
40 | renderBody = ({ loading, list }) => loading ? : this.renderList(list);
41 |
42 | render() {
43 | return (
44 |
45 |
46 | Recent destinations
47 |
48 | {this.renderBody(this.props.recents)}
49 |
50 | );
51 | }
52 | }
53 |
54 | const styles = StyleSheet.create({
55 | container: {
56 | flex: 1,
57 | alignItems: 'stretch',
58 | justifyContent: 'flex-start',
59 | flexDirection: 'column'
60 | },
61 | title: {
62 | fontSize: measures.fontSizeMedium - 3
63 | },
64 | listContainer: {
65 | borderWidth: 1,
66 | borderColor: colors.lightGray
67 | },
68 | itemContainer: {
69 | borderBottomWidth: 1,
70 | borderColor: colors.lightGray,
71 | height: 64,
72 | alignItems: 'flex-start',
73 | justifyContent: 'center',
74 | padding: measures.defaultPadding
75 | },
76 | itemTitle: {
77 | fontSize: measures.fontSizeMedium - 1
78 | },
79 | noItems: {
80 | marginTop: measures.defaultMargin * 2
81 | }
82 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Partnr
2 | This is a decentralized savings platform built to run on ethereum, using [Dai.js](https://makerdao.com/documentation/)
3 |
4 | [](https://circleci.com/gh/maschad/partnr)
5 |
6 |
7 | ## Table of Contents
8 |
9 | * [Available Scripts](#available-scripts)
10 | * [npm start](#npm-start)
11 | * [npm test](#npm-test)
12 | * [npm run ios](#npm-run-ios)
13 | * [npm run android](#npm-run-android)
14 | * [npm run android-build](#npm-run-android-build)
15 | * [npm run android-bundle](#npm-run-android-bundle)
16 | * [npm run android-clean](#npm-run-android-clean)
17 | * [Writing and Running Tests](#writing-and-running-tests)
18 | * [License](#license)
19 | * [Contribute](#contribute)
20 |
21 | ## Available Scripts
22 |
23 | ### `npm install`
24 |
25 | Installs all dependencies and prepares the app to run.
26 |
27 | ### `npm start`
28 |
29 | Runs Packager to provide your app in development mode.
30 |
31 | #### `npm test`
32 |
33 | Runs the [jest](https://github.com/facebook/jest) test runner on your tests.
34 |
35 | #### `npm run ios`
36 |
37 | Open your app in the iOS Simulator if you're on a Mac and have it installed. Depends on `npm start`.
38 |
39 | #### `npm run android`
40 |
41 | Open your app on a connected Android device or emulator. Requires an installation of Android build tools (see [React Native docs](https://facebook.github.io/react-native/docs/getting-started.html) for detailed setup). Depends on `npm start`.
42 |
43 | #### `npm run android:build`
44 |
45 | Build the Android app and generate the APK to install on the device.
46 |
47 | #### `npm run android:bundle`
48 |
49 | Bundles the ReactNative JavaScript code. Run it before running the build command to be able to run the test without depending on the development server.
50 |
51 | #### `npm run android:generate-apk`
52 |
53 | Bundle and build the Android app.
54 |
55 | #### `npm run android:clean`
56 |
57 | Clean the Android generated build files.
58 |
59 | ## Writing and Running Tests
60 |
61 | This project is set up to use [jest](https://facebook.github.io/jest/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test` extension to have the files loaded by jest. See [the template project](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/App.test.js) for an example test. The [jest documentation](https://facebook.github.io/jest/docs/getting-started.html) is also a wonderful resource, as is the [React Native testing tutorial](https://facebook.github.io/jest/docs/tutorial-react-native.html).
62 |
63 | ## License
64 |
65 | MIT
66 |
67 |
68 | ## Contribute
69 |
70 | TODO
--------------------------------------------------------------------------------
/src/common/stores/__tests__/wallets.js:
--------------------------------------------------------------------------------
1 | import { WalletsStore } from '../wallets';
2 | import { Wallet as WalletUtils } from '@common/utils';
3 |
4 | describe('WalletsStore', () => {
5 |
6 | let walletsStore;
7 |
8 | beforeEach(() => walletsStore = new WalletsStore());
9 |
10 | it('should be able to change the loading state', () => {
11 | walletsStore.isLoading(true);
12 | expect(walletsStore.loading).toBe(true);
13 | });
14 |
15 | it('should be able to add a wallet instance to the store list', () => {
16 | const mnemonics = WalletUtils.generateMnemonics();
17 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
18 | expect(walletsStore.list.length).toBe(0);
19 | walletsStore.addWallet("walletName", wallet, "description");
20 | expect(walletsStore.list.length).toBe(1);
21 | expect(walletsStore.list[0].name).toBe("walletName");
22 | expect(walletsStore.list[0].description).toBe('description');
23 | expect(walletsStore.list[0].getAddress()).toBe(wallet.getAddress());
24 | });
25 |
26 | it('should be able to modify a wallet balance in the list', () => {
27 | const mnemonics1 = WalletUtils.generateMnemonics();
28 | const wallet1 = WalletUtils.loadWalletFromMnemonics(mnemonics1);
29 | const mnemonics2 = WalletUtils.generateMnemonics();
30 | const wallet2 = WalletUtils.loadWalletFromMnemonics(mnemonics2);
31 | walletsStore.addWallet("walletName1", wallet1);
32 | walletsStore.addWallet("walletName2", wallet2);
33 | expect(walletsStore.list.length).toBe(2);
34 | expect(walletsStore.list[1].getAddress()).toBe(wallet2.getAddress());
35 | walletsStore.setBalance(wallet2.getAddress(), 1000);
36 | expect(walletsStore.list[1].balance).toBe(1000);
37 | });
38 |
39 | it('should be able to remove a wallet instance from the store list', () => {
40 | const mnemonics = WalletUtils.generateMnemonics();
41 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
42 | walletsStore.addWallet("walletName", wallet);
43 | expect(walletsStore.list.length).toBe(1);
44 | walletsStore.removeWallet(wallet);
45 | expect(walletsStore.list.length).toBe(0);
46 | });
47 |
48 | it('should be able to reset the store state', () => {
49 | const mnemonics = WalletUtils.generateMnemonics();
50 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
51 | expect(walletsStore.list.length).toBe(0);
52 | walletsStore.addWallet("walletName", wallet);
53 | walletsStore.isLoading(true);
54 | walletsStore.reset();
55 | expect(walletsStore.list.length).toBe(0);
56 | expect(walletsStore.loading).toBeFalsy();
57 | });
58 | });
--------------------------------------------------------------------------------
/src/components/views/ReceiveCoins/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Clipboard, Share, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import QRCode from 'react-native-qrcode-svg';
5 | import { Icon } from '@components/widgets';
6 | import { General as GeneralActions } from '@common/actions';
7 | import { colors, measures } from '@common/styles';
8 |
9 | @inject('wallet')
10 | @observer
11 | export class ReceiveCoins extends React.Component {
12 |
13 | copyToClipboard() {
14 | const { item } = this.props.wallet;
15 | Clipboard.setString(item.getAddress());
16 | GeneralActions.notify('Copied to clipboard', 'short');
17 | }
18 |
19 | share() {
20 | const { item } = this.props.wallet;
21 | Share.share({
22 | title: 'Wallet address:',
23 | message: item.getAddress()
24 | });
25 | }
26 |
27 | renderColumn = (icon, label, action) => (
28 |
29 |
30 |
31 | {label}
32 |
33 |
34 | );
35 |
36 | render() {
37 | const { wallet: { item } } = this.props;
38 | return (
39 |
40 | Show the code below to receive coins
41 |
42 |
43 |
44 | {item.getAddress()}
45 |
46 |
47 | {this.renderColumn('copy', 'Copy', () => this.copyToClipboard())}
48 | {this.renderColumn('share', 'Share', () => this.share())}
49 |
50 |
51 |
52 | );
53 | }
54 | }
55 |
56 | const styles = StyleSheet.create({
57 | container: {
58 | backgroundColor: colors.white,
59 | flex: 1,
60 | alignItems: 'stretch',
61 | justifyContent: 'space-around',
62 | padding: measures.defaultPadding
63 | },
64 | actions: {
65 | height: 56
66 | },
67 | actionsBar: {
68 | flexDirection: 'row',
69 | flex: 3
70 | },
71 | actionColumn: {
72 | flexDirection: 'column',
73 | flex: 1,
74 | alignItems: 'center',
75 | justifyContent: 'center'
76 | },
77 | centered: {
78 | alignSelf: 'center'
79 | }
80 | });
--------------------------------------------------------------------------------
/src/components/views/CreateMnemonics/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { Button, TextBullet } from '@components/widgets';
4 | import { Wallet as WalletUtils } from '@common/utils';
5 | import { colors, measures } from '@common/styles';
6 |
7 | export class CreateMnemonics extends React.Component {
8 |
9 | static navigationOptions = { title: 'Create Wallet' };
10 |
11 | state = { mnemonics: null };
12 |
13 | onPressProceed() {
14 | const { mnemonics } = this.state;
15 | const { walletName, walletDescription } = this.props.navigation.state.params;
16 | this.props.navigation.navigate('ConfirmMnemonics', { mnemonics, walletName, walletDescription });
17 | }
18 |
19 | onPressReveal() {
20 | const mnemonics = WalletUtils.generateMnemonics();
21 | this.setState({ mnemonics });
22 | }
23 |
24 | renderMnemonic = (mnemonic, index) => (
25 |
26 | {mnemonic}
27 |
28 | );
29 |
30 | renderBody() {
31 | const { mnemonics } = this.state;
32 | if (!mnemonics) return this.onPressReveal()}>Reveal;
33 | return (
34 |
35 | {mnemonics.map(this.renderMnemonic)}
36 |
37 | );
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
44 | Save carefully your sequence of mnemonics:
45 | {this.renderBody()}
46 |
47 | {this.state.mnemonics && (
48 | this.onPressProceed()}>Proceed
49 | )}
50 |
51 |
52 | );
53 | }
54 | }
55 |
56 | const styles = StyleSheet.create({
57 | container: {
58 | alignItems: 'center',
59 | justifyContent: 'space-between',
60 | flex: 1,
61 | backgroundColor: colors.defaultBackground,
62 | padding: measures.defaultPadding
63 | },
64 | message: {
65 | color: colors.black,
66 | fontSize: 16,
67 | textAlign: 'center',
68 | marginVertical: measures.defaultMargin,
69 | marginHorizontal: 32
70 | },
71 | mnemonicsContainer: {
72 | flexDirection: 'row',
73 | justifyContent: 'center',
74 | flexWrap: 'wrap',
75 | maxWidth: '80%'
76 | },
77 | mnemonic: {
78 | margin: 4
79 | },
80 | buttonsContainer: {
81 | width: '100%',
82 | justifyContent: 'flex-end',
83 | height: 104
84 | }
85 | });
--------------------------------------------------------------------------------
/src/components/views/ConfirmMnemonics/ConfirmBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, TouchableOpacity, View, Text } from 'react-native';
3 | import _ from 'lodash';
4 | import { TextBullet } from '@components/widgets';
5 | import { colors } from '@common/styles';
6 |
7 | export default class ConfirmBox extends React.Component {
8 |
9 | state = { selectable: [], selected: [] };
10 |
11 | isValidSequence() {
12 | return _.isEqual(this.props.mnemonics, this.state.selected);
13 | }
14 |
15 | static getDerivedStateFromProps(nextProps, prevState) {
16 | const newState = { ...prevState };
17 | if (!prevState.selectable.length && !prevState.selected.length) {
18 | newState.selectable = _.shuffle([...nextProps.mnemonics]);
19 | }
20 | return newState;
21 | }
22 |
23 | onPressMnemonic(mnemonic, isSelected) {
24 | const { selectable, selected } = this.state;
25 | if (isSelected) this.setState({
26 | selectable: selectable.filter(m => m !== mnemonic),
27 | selected: selected.concat([mnemonic])
28 | });
29 | else this.setState({
30 | selectable: selectable.concat([mnemonic]),
31 | selected: selected.filter(m => m !== mnemonic)
32 | });
33 | }
34 |
35 | renderMnemonic = (mnemonic, index, selected) => (
36 | this.onPressMnemonic(mnemonic, selected)}>
37 |
38 | {mnemonic}
39 |
40 |
41 | );
42 |
43 | renderSelected = () => (
44 |
45 | {this.state.selected.map((mnemonic, index) => this.renderMnemonic(mnemonic, index, false))}
46 |
47 | );
48 |
49 | renderSelectable = () => (
50 |
51 | {this.state.selectable.map((mnemonic, index) => this.renderMnemonic(mnemonic, index, true))}
52 |
53 | );
54 |
55 | render() {
56 | return (
57 |
58 | Sequence:
59 | {this.renderSelected()}
60 | Click on the words in the correct order:
61 | {this.renderSelectable()}
62 |
63 | );
64 | }
65 | }
66 |
67 | const styles = StyleSheet.create({
68 | container: {
69 | flexDirection: 'column',
70 | alignItems: 'center'
71 | },
72 | mnemonics: {
73 | flexDirection: 'row',
74 | flexWrap: 'wrap',
75 | justifyContent: 'center',
76 | margin: 4
77 | },
78 | mnemonic: {
79 | margin: 4
80 | },
81 | text: {
82 | color: colors.black
83 | }
84 | });
--------------------------------------------------------------------------------
/src/components/views/WalletsOverview/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FlatList, RefreshControl, StyleSheet, View } from 'react-native';
3 |
4 | import { inject, observer } from 'mobx-react';
5 | import { HeaderIcon } from '@components/widgets';
6 | import { colors, measures } from '@common/styles';
7 |
8 | import { Container, Card, CardItem, Text, Body } from 'native-base';
9 | import { Grid } from 'react-native-easy-grid';
10 |
11 | import { General as GeneralActions, Wallets as WalletActions, Prices as PricesActions } from '@common/actions';
12 |
13 |
14 | import NoWallets from './NoWallets';
15 | import TotalBalance from './TotalBalance';
16 | import WalletCard from './WalletCard';
17 |
18 | @inject('prices', 'wallets')
19 | @observer
20 | export class WalletsOverview extends React.Component {
21 |
22 | static navigationOptions = ({ navigation, screenProps }) => ({
23 | title: 'Home',
24 | headerRight: (
25 | navigation.navigate('Settings')} />
31 | )
32 | });
33 |
34 | get loading() {
35 | return this.props.prices.loading || this.props.wallets.loading;
36 | }
37 |
38 | componentDidMount() {
39 | this.populate();
40 | }
41 |
42 | async populate() {
43 | try {
44 | await Promise.all([
45 | WalletActions.loadWallets(),
46 | PricesActions.getPrice()
47 | ]);
48 | } catch (e) {
49 | GeneralActions.notify(e.message, 'long');
50 | }
51 | }
52 |
53 | onPressWallet(wallet) {
54 | if (this.loading) return;
55 | WalletActions.selectWallet(wallet);
56 | this.props.navigation.navigate('WalletDetails', { wallet });
57 | }
58 |
59 | renderItem = ({ item }) => this.onPressWallet(item)} />
60 |
61 | renderBody = (list) => (
62 |
63 |
64 | this.populate()} />}
68 | keyExtractor={(item, index) => String(index)}
69 | renderItem={this.renderItem} />
70 |
71 |
72 | );
73 |
74 | render() {
75 | const { list } = this.props.wallets;
76 | const { navigation } = this.props;
77 | return (
78 |
79 | {(!list.length && !this.loading) ? : this.renderBody(list)}
80 |
81 | );
82 | }
83 | }
84 |
85 | const styles = StyleSheet.create({
86 | content: {
87 | marginTop: measures.defaultMargin
88 | }
89 | });
--------------------------------------------------------------------------------
/src/components/widgets/Camera/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, TouchableWithoutFeedback, Vibration, View } from 'react-native';
3 | import { RNCamera } from 'react-native-camera';
4 | import Modal from 'react-native-modal';
5 | import Permissions from 'react-native-permissions';
6 | import { Icon } from '@components/widgets';
7 | import { colors } from '@common/styles';
8 |
9 | const { BarCodeType } = RNCamera.Constants;
10 |
11 | export class Camera extends React.Component {
12 |
13 | state = { isModalVisible: false };
14 |
15 | async show() {
16 | var status;
17 | try {
18 | status = await Permissions.check('camera');
19 | if (status === 'authorized') this.setState({ isModalVisible: true });
20 | else {
21 | status = await Permissions.request('camera');
22 | if (status === 'authorized') this.setState({ isModalVisible: true });
23 | else throw new Error('Not allowed to use the camera.');
24 | }
25 | } catch (e) {
26 | console.error(e);
27 | this.setState({ isModalVisible: false });
28 | }
29 | }
30 |
31 | hide() {
32 | this.setState({ isModalVisible: false });
33 | }
34 |
35 | onBarCodeRead({ type, data }) {
36 | if (type === BarCodeType.qr) {
37 | Vibration.vibrate();
38 | this.hide();
39 | this.props.onBarCodeRead(data);
40 | }
41 | }
42 |
43 | renderView = (onClose) => (
44 |
45 | this.onBarCodeRead(data)} />
49 |
50 |
51 |
52 |
53 |
54 | );
55 |
56 | render() {
57 | const { modal, onClose } = this.props;
58 | return !modal ? this.renderView(onClose) : (
59 |
63 | );
64 | }
65 | }
66 |
67 | const styles = StyleSheet.create({
68 | container: {
69 | flex: 1,
70 | alignItems: 'stretch',
71 | justifyContent: 'center',
72 | },
73 | camera: {
74 | flex: 1
75 | },
76 | closeIcon: {
77 | position: 'absolute',
78 | zIndex: 1,
79 | top: 8,
80 | right: 10
81 | },
82 | marker: {
83 | position: 'absolute',
84 | alignSelf: 'center',
85 | zIndex: 1,
86 | width: 200,
87 | height: 200,
88 | borderWidth: 4,
89 | borderColor: 'green'
90 | }
91 | });
--------------------------------------------------------------------------------
/src/components/views/NewWalletName/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Keyboard, StyleSheet, Text, TextInput, View } from 'react-native';
3 | import { Button } from '@components/widgets';
4 | import { colors, measures } from '@common/styles';
5 |
6 | export class NewWalletName extends React.Component {
7 |
8 | static navigationOptions = { title: 'New Wallet Name' };
9 |
10 | state = { walletName: '', walletDescription: '' };
11 |
12 | onPressContinue() {
13 | Keyboard.dismiss();
14 | const { walletName, walletDescription } = this.state;
15 | if (!walletName) return;
16 | this.props.navigation.navigate('NewWallet', { walletName, walletDescription });
17 | }
18 |
19 | render() {
20 | return (
21 |
22 |
23 | Give a name to the new wallet
24 | this.setState({ walletName })} />
29 | Give a description too (optional)
30 | this.setState({ walletDescription })} />
35 |
36 |
37 | this.onPressContinue()} />
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | const styles = StyleSheet.create({
47 | container: {
48 | flex: 1,
49 | alignItems: 'stretch',
50 | justifyContent: 'space-between',
51 | backgroundColor: colors.defaultBackground,
52 | padding: measures.defaultPadding
53 | },
54 | body: {
55 | flex: 1,
56 | alignItems: 'center',
57 | justifyContent: 'center'
58 | },
59 | message: {
60 | color: colors.black,
61 | fontSize: 16,
62 | textAlign: 'center',
63 | marginVertical: measures.defaultMargin,
64 | marginHorizontal: 32
65 | },
66 | buttonsContainer: {
67 | width: '100%',
68 | justifyContent: 'space-between',
69 | height: 52
70 | },
71 | input: {
72 | width: '90%',
73 | borderBottomWidth: 1,
74 | borderBottomColor: colors.black,
75 | padding: 4,
76 | paddingLeft: 0,
77 | marginRight: 2,
78 | textAlign: 'center',
79 | color: colors.black
80 | }
81 | });
--------------------------------------------------------------------------------
/src/components/views/LoadPrivateKey/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Keyboard, StyleSheet, Text, View } from 'react-native';
3 | import { Button, Camera, InputWithIcon } from '@components/widgets';
4 | import { colors, measures } from '@common/styles';
5 | import { Wallet as WalletUtils } from '@common/utils';
6 | import { General as GeneralActions, Wallets as WalletsActions } from '@common/actions';
7 |
8 | export class LoadPrivateKey extends React.Component {
9 |
10 | static navigationOptions = { title: 'Load Wallet' };
11 |
12 | state = { pk: '' };
13 |
14 | async onPressOpenWallet() {
15 | if (!this.state.pk) return;
16 | Keyboard.dismiss();
17 | try {
18 | const wallet = WalletUtils.loadWalletFromPrivateKey(this.state.pk);
19 | const { walletName, walletDescription } = this.props.navigation.state.params;
20 | await WalletsActions.addWallet(walletName, wallet, walletDescription);
21 | this.props.navigation.navigate('WalletsOverview', { replaceRoute: true });
22 | await WalletsActions.saveWallets();
23 | } catch (e) {
24 | GeneralActions.notify(e.message, 'long');
25 | }
26 | }
27 |
28 | render() {
29 | return (
30 |
31 |
32 | Private key
33 | this.setState({ pk })}
38 | onPressIcon={() => this.refs.camera.show()} />
39 |
40 |
41 | this.onPressOpenWallet()} />
44 |
45 | this.refs.camera.hide()}
49 | onBarCodeRead={data => this.refs.input.onChangeText(data)} />
50 |
51 | );
52 | }
53 | }
54 |
55 | const styles = StyleSheet.create({
56 | container: {
57 | flex: 1,
58 | alignItems: 'center',
59 | justifyContent: 'space-between',
60 | backgroundColor: colors.defaultBackground,
61 | padding: measures.defaultPadding
62 | },
63 | body: {
64 | flex: 1,
65 | alignItems: 'center',
66 | justifyContent: 'center'
67 | },
68 | message: {
69 | color: colors.black,
70 | fontSize: 16,
71 | textAlign: 'center',
72 | marginVertical: measures.defaultMargin,
73 | marginHorizontal: 32
74 | },
75 | buttonsContainer: {
76 | width: '100%',
77 | justifyContent: 'space-between',
78 | height: 52
79 | }
80 | });
--------------------------------------------------------------------------------
/src/common/actions/__tests__/transactions.js:
--------------------------------------------------------------------------------
1 | import { wallet as WalletStore } from '@common/stores';
2 | import { Wallet as WalletUtils, Transaction as TransactionUtils } from '@common/utils';
3 | import * as Transactions from '../transactions';
4 | import ethers from 'ethers';
5 |
6 | const WALLET_PK = '62384683889eae6de8440eb735856f31bb4f17815888f847c8567b3c87f00be8';
7 | const DESTINATION_ADDRESS = '0x407428BF09ea7Dac2824A64AfE88171041a02b14';
8 |
9 | describe('TransactionsActions', () => {
10 |
11 | beforeEach(() => WalletStore.reset());
12 |
13 | it('`sendEther` should break if the the destination address or the value are invalid', async function() {
14 | const mnemonics = WalletUtils.generateMnemonics();
15 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
16 | try {
17 | await Transactions.sendEther(null, '0x12345', '5.0');
18 | fail('should have thrown an Error.');
19 | } catch (e) { expect(e.message).toBe('Invalid wallet'); }
20 |
21 | try {
22 | await Transactions.sendEther(wallet, null, '5.0');
23 | fail('should have thrown an Error.');
24 | } catch (e) { expect(e.message).toBe('Invalid destination address'); }
25 | });
26 |
27 | it.skip('`sendEther` should send ether to some address when there are funds available', async function() {
28 | jest.setTimeout(60000);
29 | const wallet = WalletUtils.loadWalletFromPrivateKey(WALLET_PK);
30 | const value = '0.002';
31 | try {
32 | const txn = await Transactions.sendEther(wallet, DESTINATION_ADDRESS, value);
33 | expect(txn.from).toBe(wallet.getAddress());
34 | expect(txn.to).toBe(DESTINATION_ADDRESS);
35 | expect(txn.value.toString()).toBe(ethers.utils.parseEther(value).toString());
36 | expect(WalletStore.history.length).toBe(1);
37 | } catch (e) { fail(e); }
38 | });
39 |
40 | it('`sendTransaction` should break if the the wallet or the transaction is invalid', async function() {
41 | const mnemonics = WalletUtils.generateMnemonics();
42 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
43 | try {
44 | await Transactions.sendTransaction(null);
45 | fail('should have thrown an Error.');
46 | } catch (e) { expect(e.message).toBe('Invalid wallet'); }
47 |
48 | try {
49 | await Transactions.sendEther(wallet, {});
50 | fail('should have thrown an Error.');
51 | } catch (e) { expect(e.message).toBe('Invalid destination address'); }
52 | });
53 |
54 | it.skip('`sendTransaction` should send ether to some address when there are funds available', async function() {
55 | jest.setTimeout(60000);
56 | const wallet = WalletUtils.loadWalletFromPrivateKey(WALLET_PK);
57 | const value = '0.002';
58 | let txn = TransactionUtils.createTransaction(DESTINATION_ADDRESS, value);
59 | try {
60 | txn = await Transactions.sendTransaction(wallet, txn);
61 | expect(txn.from).toBe(wallet.getAddress());
62 | expect(txn.to).toBe(DESTINATION_ADDRESS);
63 | expect(txn.value.toString()).toBe(ethers.utils.parseEther(value).toString());
64 | expect(WalletStore.history.length).toBe(1);
65 | } catch (e) { fail(e); }
66 | });
67 | });
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem http://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/src/components/views/WalletSettings/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { Icon } from '@components/widgets';
5 | import { colors, measures } from '@common/styles';
6 | import { General as GeneralActions, Wallets as WalletsActions } from '@common/actions';
7 | import ListItem from './ListItem';
8 |
9 | @inject('wallet')
10 | @observer
11 | export class WalletSettings extends React.Component {
12 |
13 | async removeWallet() {
14 | try {
15 | const { wallet } = this.props;
16 | await WalletsActions.removeWallet(wallet.item);
17 | this.props.navigation.goBack();
18 | await WalletsActions.saveWallets();
19 | } catch (e) {
20 | GeneralActions.notify(e.message, 'long');
21 | }
22 | }
23 |
24 | showPK() {
25 | const { wallet } = this.props;
26 | this.props.navigation.push('ShowPrivateKey', { wallet });
27 | }
28 |
29 | confirmRemoveWallet() {
30 | Alert.alert(
31 | 'Remove wallet',
32 | 'This action cannot be undone. Are you sure?',
33 | [
34 | { text: 'Cancel', onPress: () => {}, style: 'cancel' },
35 | { text: 'Remove', onPress: () => this.removeWallet() }
36 | ],
37 | { cancelable: false }
38 | );
39 | }
40 |
41 | confirmExportPK() {
42 | Alert.alert(
43 | 'Export private key',
44 | 'Make sure you are alone and no one else will see your private key.',
45 | [
46 | { text: 'Cancel', onPress: () => {}, style: 'cancel' },
47 | { text: 'Continue', onPress: () => this.showPK() }
48 | ],
49 | { cancelable: false }
50 | );
51 | }
52 |
53 | render() {
54 | return (
55 |
56 | this.confirmExportPK()}>
57 |
58 |
59 |
60 |
61 | Export private key
62 |
63 |
64 | this.confirmRemoveWallet()}>
65 |
66 |
67 |
68 |
69 | Remove wallet
70 |
71 |
72 |
73 | );
74 | }
75 | }
76 |
77 | const styles = StyleSheet.create({
78 | container: {
79 | backgroundColor: colors.defaultBackground,
80 | flex: 1
81 | },
82 | itemContainer: {
83 | flexDirection: 'row',
84 | alignItems: 'center',
85 | justifyContent: 'flex-start'
86 | },
87 | icon: {
88 | width: 24,
89 | height: 24,
90 | margin: measures.defaultMargin
91 | },
92 | itemTitle: {
93 | fontSize: measures.fontSizeMedium
94 | }
95 | });
--------------------------------------------------------------------------------
/src/components/views/WalletsOverview/WalletCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { Icon } from '@components/widgets';
5 | import { colors, measures } from '@common/styles';
6 | import { Wallet as WalletUtils } from '@common/utils';
7 | import { Wallets as WalletActions } from '@common/actions';
8 |
9 | @inject('prices')
10 | @observer
11 | export default class WalletCard extends React.Component {
12 |
13 | get balance() {
14 | if (!this.props.wallet.balance) return 0;
15 | return Number(WalletUtils.formatBalance(this.props.wallet.balance));
16 | }
17 |
18 | get fiatBalance() {
19 | return Number(this.props.prices.usd * this.balance);
20 | }
21 |
22 | componentDidMount() {
23 | WalletActions.updateBalance(this.props.wallet);
24 | }
25 |
26 | render() {
27 | const { onPress, wallet } = this.props;
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 | {wallet.name}
36 | {wallet.description}
37 |
38 |
39 |
40 | {this.balance.toFixed(3)}
41 | US$ {this.fiatBalance.toFixed(2)}
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | const styles = StyleSheet.create({
51 | container: {
52 | alignItems: 'stretch',
53 | backgroundColor: colors.white,
54 | flexDirection: 'row',
55 | alignItems: 'center',
56 | paddingHorizontal: measures.defaultPadding,
57 | marginBottom: measures.defaultMargin,
58 | height: 70
59 | },
60 | leftColumn: {
61 | width: 40,
62 | alignItems: 'flex-start',
63 | justifyContent: 'center'
64 | },
65 | middleColumn: {
66 | flex: 2
67 | },
68 | rightColumn: {
69 | flex: 1,
70 | justifyContent: 'flex-end',
71 | flexDirection: 'row'
72 | },
73 | title: {
74 | fontSize: measures.fontSizeMedium,
75 | color: colors.gray,
76 | fontWeight: 'bold'
77 | },
78 | description: {
79 | fontSize: measures.fontSizeMedium - 2,
80 | color: colors.gray,
81 | },
82 | balanceContainer: {
83 | alignItems: 'flex-end',
84 | justifyContent: 'space-between',
85 | flexDirection: 'column'
86 | },
87 | balance: {
88 | fontSize: measures.fontSizeMedium - 1,
89 | color: colors.gray,
90 | marginLeft: measures.defaultMargin,
91 | fontWeight: 'bold'
92 | },
93 | fiatbalance: {
94 | fontSize: measures.fontSizeMedium - 3,
95 | color: colors.gray,
96 | marginLeft: measures.defaultMargin
97 | },
98 | next: {
99 | color: colors.lightGray
100 | }
101 | });
--------------------------------------------------------------------------------
/ios/Partnr/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/components/views/LoadMnemonics/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Keyboard, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
3 | import { Button, InputWithIcon, TextBullet } from '@components/widgets';
4 | import { colors, measures } from '@common/styles';
5 | import { Wallet as WalletUtils } from '@common/utils';
6 | import { General as GeneralActions, Wallets as WalletsActions } from '@common/actions';
7 |
8 | export class LoadMnemonics extends React.Component {
9 |
10 | static navigationOptions = { title: 'Load Wallet' };
11 |
12 | state = { mnemonics: [] };
13 |
14 | async onPressOpenWallet() {
15 | if (!this.state.mnemonics.length) return;
16 | Keyboard.dismiss();
17 | try {
18 | const { mnemonics } = this.state;
19 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
20 | const { walletName, walletDescription } = this.props.navigation.state.params;
21 | await WalletsActions.addWallet(walletName, wallet, walletDescription);
22 | this.props.navigation.navigate('WalletsOverview', { replaceRoute: true });
23 | await WalletsActions.saveWallets();
24 | } catch (e) {
25 | GeneralActions.notify(e.message, 'long');
26 | }
27 | }
28 |
29 | removeMnemonic(mnemonic) {
30 | let { mnemonics } = this.state;
31 | mnemonics = mnemonics.filter(m => m !== mnemonic);
32 | this.setState({ mnemonics });
33 | }
34 |
35 | renderMnemonic = (mnemonic, index) => (
36 | this.removeMnemonic(mnemonic)}>
37 |
38 | {mnemonic}
39 |
40 |
41 | );
42 |
43 | render() {
44 | return (
45 |
46 |
47 | Type the mnemonics
48 |
49 | {this.state.mnemonics.map(this.renderMnemonic)}
50 |
51 | this.setState({ mnemonics: this.state.mnemonics.concat([text]) })} />
55 |
56 |
57 | this.onPressOpenWallet()} />
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | const styles = StyleSheet.create({
67 | container: {
68 | alignItems: 'stretch',
69 | justifyContent: 'space-between',
70 | flex: 1,
71 | backgroundColor: colors.defaultBackground
72 | },
73 | body: {
74 | flex: 1,
75 | alignItems: 'center',
76 | justifyContent: 'space-between',
77 | },
78 | message: {
79 | color: colors.black,
80 | fontSize: 16,
81 | textAlign: 'center',
82 | marginVertical: measures.defaultMargin,
83 | marginHorizontal: 32
84 | },
85 | logo: {
86 | width: 128,
87 | height: 128,
88 | },
89 | buttonsContainer: {
90 | width: '100%',
91 | justifyContent: 'space-between',
92 | height: 52
93 | },
94 | mnemonics: {
95 | flexDirection: 'row',
96 | flexWrap: 'wrap',
97 | justifyContent: 'center',
98 | margin: 4
99 | },
100 | mnemonic: {
101 | margin: 4
102 | }
103 | });
--------------------------------------------------------------------------------
/src/common/actions/__tests__/wallets.js:
--------------------------------------------------------------------------------
1 | import { wallet as WalletStore, wallets as WalletsStore } from '@common/stores';
2 | import { Wallet as WalletUtils } from '@common/utils';
3 | import * as Action from '../wallets';
4 |
5 | describe('WalletsActions', () => {
6 |
7 | it('should add a wallet to the store', async function() {
8 | const mnemonics = WalletUtils.generateMnemonics();
9 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
10 | try {
11 | await Action.addWallet("walletName", wallet);
12 | } catch (e) {
13 | fail(e);
14 | } finally {
15 | WalletsStore.reset();
16 | }
17 | });
18 |
19 | it('should fail to add a wallet if not ethers.Wallet instance', async function() {
20 | const wallet = { privateKey: '54321', address: '123445' };
21 | try {
22 | await Action.addWallet("walletName", wallet);
23 | fail('Should have thrown an error.');
24 | }
25 | catch (e) {
26 | expect(e.message).toEqual('Invalid Wallet');
27 | } finally {
28 | WalletsStore.reset();
29 | }
30 | });
31 |
32 | it('should be able to update a wallet balance', async function() {
33 | const mnemonics = WalletUtils.generateMnemonics();
34 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
35 | try {
36 | await Action.addWallet("walletName", wallet);
37 | await Action.updateBalance(wallet);
38 | expect(WalletsStore.list[0].getAddress()).toBe(wallet.getAddress());
39 | expect(WalletsStore.list[0].balance).toBeInstanceOf(Object);
40 | } catch (e) {
41 | fail(e);
42 | } finally {
43 | WalletsStore.reset();
44 | }
45 | });
46 |
47 | it('should select a wallet in the store', async function() {
48 | const mnemonics = WalletUtils.generateMnemonics();
49 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
50 | try {
51 | await Action.selectWallet(wallet);
52 | } catch (e) {
53 | fail(e);
54 | } finally {
55 | WalletsStore.reset();
56 | }
57 | });
58 |
59 | it('should fail to add a wallet if not ethers.Wallet instance', async function() {
60 | const wallet = { privateKey: '54321', address: '123445' };
61 | try {
62 | await Action.selectWallet(wallet);
63 | fail('Should have thrown an error.');
64 | }
65 | catch (e) {
66 | expect(e.message).toEqual('Invalid Wallet');
67 | } finally {
68 | WalletsStore.reset();
69 | }
70 | });
71 |
72 | it('should remove an existing wallet from the store', async function() {
73 | const mnemonics = WalletUtils.generateMnemonics();
74 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
75 | try {
76 | await Action.addWallet("walletName", wallet);
77 | expect(WalletsStore.list.length).toBe(1);
78 | await Action.removeWallet(wallet);
79 | expect(WalletsStore.list.length).toBe(0);
80 | } catch (e) {
81 | fail(e);
82 | } finally {
83 | WalletsStore.reset();
84 | }
85 | });
86 |
87 | it('should be able to update a wallet transactions history', async function() {
88 | const mnemonics = WalletUtils.generateMnemonics();
89 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
90 | try {
91 | await Action.addWallet("walletName", wallet);
92 | await Action.updateHistory(wallet);
93 | expect(WalletStore.history).toBeInstanceOf(Array);
94 | } catch (e) {
95 | fail(e);
96 | } finally {
97 | WalletStore.reset();
98 | WalletsStore.reset();
99 | }
100 | });
101 | });
--------------------------------------------------------------------------------
/src/common/services/__tests__/transaction.js:
--------------------------------------------------------------------------------
1 | import { Wallet as WalletUtils, Transaction as TransactionUtils } from '@common/utils';
2 | import * as Transactions from '../transactions';
3 | import ethers from 'ethers';
4 |
5 | describe.skip('TransactionsService', () => {
6 |
7 | it('`sendTransaction` should break if the wallet is invalid', async function() {
8 | const wallet = null;
9 | const txn = {};
10 | try {
11 | await Transactions.sendTransaction(wallet, txn);
12 | fail('should have thrown an Error.');
13 | } catch (e) {
14 | expect(e.message).toBe('Invalid wallet');
15 | }
16 | });
17 |
18 | it('`sendTransaction` should break if the transaction is invalid', async function() {
19 | const mnemonics = WalletUtils.generateMnemonics();
20 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
21 | try {
22 | await Transactions.sendTransaction(wallet, {});
23 | fail('should have thrown an Error.');
24 | } catch (e) { expect(e.message).toBe('Invalid transaction'); }
25 |
26 | try {
27 | await Transactions.sendTransaction(wallet, []);
28 | fail('should have thrown an Error.');
29 | } catch (e) { expect(e.message).toBe('Invalid transaction'); }
30 |
31 | try {
32 | await Transactions.sendTransaction(wallet, null);
33 | fail('should have thrown an Error.');
34 | } catch (e) { expect(e.message).toBe('Invalid transaction'); }
35 |
36 | try {
37 | await Transactions.sendTransaction(wallet, undefined);
38 | fail('should have thrown an Error.');
39 | } catch (e) { expect(e.message).toBe('Invalid transaction'); }
40 |
41 | try {
42 | await Transactions.sendTransaction(wallet, true);
43 | fail('should have thrown an Error.');
44 | } catch (e) { expect(e.message).toBe('Invalid transaction'); }
45 |
46 | try {
47 | await Transactions.sendTransaction(wallet, 'foo');
48 | fail('should have thrown an Error.');
49 | } catch (e) { expect(e.message).toBe('Invalid transaction'); }
50 |
51 | try {
52 | await Transactions.sendTransaction(wallet, 42);
53 | fail('should have thrown an Error.');
54 | } catch (e) { expect(e.message).toBe('Invalid transaction'); }
55 |
56 | try {
57 | await Transactions.sendTransaction(wallet, { value: 'foo', gasLimit: 'bar', to: null });
58 | fail('should have thrown an Error.');
59 | } catch (e) { expect(e.message).toBe('Invalid transaction'); }
60 | });
61 |
62 | it('`sendTransaction` should break if the the wallet has no funds to make the transaction', async function() {
63 | const mnemonics = WalletUtils.generateMnemonics();
64 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
65 | const txn = TransactionUtils.createTransaction(wallet.getAddress(), '5.0');
66 | try {
67 | await Transactions.sendTransaction(wallet, txn);
68 | fail('should have thrown an Error.');
69 | } catch (e) { expect(e.message).toBe('insufficient funds for gas * price + value'); }
70 | });
71 |
72 | it('`sendTransaction` should send ether to some address when there are funds available', async function() {
73 | const pk = '62384683889eae6de8440eb735856f31bb4f17815888f847c8567b3c87f00be8';
74 | const wallet = WalletUtils.loadWalletFromPrivateKey(pk);
75 | const to = '0x407428BF09ea7Dac2824A64AfE88171041a02b14';
76 | const value = '0.002';
77 | const transaction = TransactionUtils.createTransaction(to, value);
78 | try {
79 | const txn = await Transactions.sendTransaction(wallet, transaction);
80 | expect(txn.from).toBe(wallet.getAddress());
81 | expect(txn.to).toBe(to);
82 | expect(txn.value.toString()).toBe(ethers.utils.parseEther(value).toString());
83 | } catch (e) { fail(e); }
84 | });
85 |
86 | it('`sendEther` should break if the the destination address or the value are invalid', async function() {
87 | const mnemonics = WalletUtils.generateMnemonics();
88 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics);
89 | try {
90 | await Transactions.sendEther(null, '0x12345', '5.0');
91 | fail('should have thrown an Error.');
92 | } catch (e) { expect(e.message).toBe('Invalid wallet'); }
93 |
94 | try {
95 | await Transactions.sendEther(wallet, null, '5.0');
96 | fail('should have thrown an Error.');
97 | } catch (e) { expect(e.message).toBe('Invalid destination address'); }
98 | });
99 |
100 | it('`sendEther` should send ether to some address when there are funds available', async function() {
101 | const pk = '62384683889eae6de8440eb735856f31bb4f17815888f847c8567b3c87f00be8';
102 | const wallet = WalletUtils.loadWalletFromPrivateKey(pk);
103 | const to = '0x407428BF09ea7Dac2824A64AfE88171041a02b14';
104 | const value = '0.002';
105 | try {
106 | const txn = await Transactions.sendEther(wallet, to, value);
107 | expect(txn.from).toBe(wallet.getAddress());
108 | expect(txn.to).toBe(to);
109 | expect(txn.value.toString()).toBe(ethers.utils.parseEther(value).toString());
110 | } catch (e) { fail(e); }
111 | });
112 | });
--------------------------------------------------------------------------------
/src/components/views/WalletExtract/TransactionCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, TouchableHighlight, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import moment from 'moment';
5 | import { Icon } from '@components/widgets';
6 | import { colors, measures } from '@common/styles';
7 | import { Wallet as WalletUtils } from '@common/utils';
8 | import TransactionDetails from './TransactionDetails';
9 |
10 | @inject('prices')
11 | @observer
12 | export default class TransactionCard extends React.Component {
13 |
14 | get isReceiving() {
15 | return this.to.toLowerCase() === this.props.walletAddress.toLowerCase();
16 | }
17 |
18 | get isConfirmed() {
19 | return this.props.transaction.confirmations > 0;
20 | }
21 |
22 | get from() {
23 | return this.props.transaction.from;
24 | }
25 |
26 | get to() {
27 | return this.props.transaction.to;
28 | }
29 |
30 | get iconName() {
31 | return (this.isReceiving) ? 'download' : 'upload';
32 | }
33 |
34 | get balance() {
35 | return Number(WalletUtils.formatBalance(this.props.transaction.value));
36 | }
37 |
38 | get fiatBalance() {
39 | return Number(this.props.prices.usd * this.balance).toFixed(2);
40 | }
41 |
42 | get timestamp() {
43 | return (this.props.transaction.timeStamp) ?
44 | moment.unix(this.props.transaction.timeStamp).format('DD/MM/YYYY hh:mm:ss') : 'Pending';
45 | }
46 |
47 | renderTransactionOperator = () => (
48 |
53 | );
54 |
55 | renderConfirmationStatus() {
56 | return this.isConfirmed ?
57 | :
58 |
59 | }
60 |
61 | render() {
62 | const { transaction, walletAddress } = this.props;
63 | return (
64 | this.refs.details.wrappedInstance.show()}>
65 |
66 |
67 |
68 |
69 |
70 | {this.renderTransactionOperator()}
71 | {this.timestamp}
72 |
73 |
74 |
75 |
80 | US$ {this.fiatBalance}
81 |
82 |
83 | {this.renderConfirmationStatus()}
84 |
85 |
86 |
90 |
91 |
92 | );
93 | }
94 | }
95 |
96 | const styles = StyleSheet.create({
97 | container: {
98 | flex: 1,
99 | alignItems: 'center',
100 | justifyContent: 'flex-start',
101 | flexDirection: 'row',
102 | backgroundColor: colors.secondary,
103 | height: 64,
104 | marginBottom: measures.defaultMargin,
105 | },
106 | leftColumn: {
107 | width: 40,
108 | alignItems: 'center',
109 | justifyContent: 'center',
110 | borderRadius: 100
111 | },
112 | centerColumn: {
113 | flex: 1,
114 | height: 64,
115 | flexDirection: 'column',
116 | alignItems: 'flex-start',
117 | justifyContent: 'center'
118 | },
119 | operatorLabel: {
120 | fontWeight: 'bold',
121 | fontSize: measures.fontSizeMedium
122 | },
123 | rightColumn: {
124 | paddingHorizontal: measures.defaultPadding,
125 | width: 150,
126 | flexDirection: 'row',
127 | },
128 | amountContainer: {
129 | flex: 1,
130 | flexDirection: 'column',
131 | alignItems: 'flex-end'
132 | },
133 | confirmationsContainer: {
134 | marginLeft: measures.defaultMargin,
135 | alignItems: 'center',
136 | justifyContent: 'center',
137 | width: 20
138 | },
139 | amountLabel: {
140 | fontWeight: 'bold',
141 | fontSize: measures.fontSizeMedium
142 | },
143 | fiatLabel: {
144 | fontSize: measures.fontSizeMedium - 4
145 | }
146 | });
--------------------------------------------------------------------------------
/src/common/stores/__tests__/prices.js:
--------------------------------------------------------------------------------
1 | import { PricesStore } from '../prices';
2 |
3 | describe('PricesStore', () => {
4 |
5 | let pricesStore;
6 |
7 | beforeEach(() => pricesStore = new PricesStore());
8 |
9 | it('should be able to change the loading state', () => {
10 | pricesStore.isLoading(true);
11 | expect(pricesStore.loading).toBe(true);
12 | });
13 |
14 | it('should be able to change the usd price', () => {
15 | pricesStore.setUSDRate(350);
16 | expect(pricesStore.usd).toBe(350);
17 | expect(pricesStore.eur).toBe(0);
18 | expect(pricesStore.jmd).toBe(0);
19 | });
20 |
21 | it('should be able to change the eur price', () => {
22 | pricesStore.setEURRate(300);
23 | expect(pricesStore.eur).toBe(300);
24 | expect(pricesStore.usd).toBe(0);
25 | expect(pricesStore.jmd).toBe(0);
26 | });
27 |
28 | it('should be able to change the jmd price', () => {
29 | pricesStore.setJMDRate(300);
30 | expect(pricesStore.jmd).toBe(300);
31 | expect(pricesStore.usd).toBe(0);
32 | expect(pricesStore.eur).toBe(0);
33 | });
34 |
35 | it('should fail to change the USD price if a NaN is given', () => {
36 | try {
37 | pricesStore.setUSDRate("whatever");
38 | fail('Should have thrown an error');
39 | } catch(e) { expect(e.message).toBe('The input is NaN') }
40 | try {
41 | pricesStore.setUSDRate(true);
42 | fail('Should have thrown an error');
43 | } catch(e) { expect(e.message).toBe('The input is NaN') }
44 | try {
45 | pricesStore.setUSDRate(false);
46 | fail('Should have thrown an error');
47 | } catch(e) { expect(e.message).toBe('The input is NaN') }
48 | try {
49 | pricesStore.setUSDRate({});
50 | fail('Should have thrown an error');
51 | } catch(e) { expect(e.message).toBe('The input is NaN') }
52 | try {
53 | pricesStore.setUSDRate([]);
54 | fail('Should have thrown an error');
55 | } catch(e) { expect(e.message).toBe('The input is NaN') }
56 | expect(pricesStore.usd).toBe(0);
57 | });
58 |
59 | it('should fail to change the EUR price if a NaN is given', () => {
60 | try {
61 | pricesStore.setEURRate("whatever");
62 | fail('Should have thrown an error');
63 | } catch(e) { expect(e.message).toBe('The input is NaN') }
64 | try {
65 | pricesStore.setEURRate(true);
66 | fail('Should have thrown an error');
67 | } catch(e) { expect(e.message).toBe('The input is NaN') }
68 | try {
69 | pricesStore.setEURRate(false);
70 | fail('Should have thrown an error');
71 | } catch(e) { expect(e.message).toBe('The input is NaN') }
72 | try {
73 | pricesStore.setEURRate({});
74 | fail('Should have thrown an error');
75 | } catch(e) { expect(e.message).toBe('The input is NaN') }
76 | try {
77 | pricesStore.setEURRate([]);
78 | fail('Should have thrown an error');
79 | } catch(e) { expect(e.message).toBe('The input is NaN') }
80 | expect(pricesStore.eur).toBe(0);
81 | });
82 |
83 | it('should fail to change the JMD price if a NaN is given', () => {
84 | try {
85 | pricesStore.setJMDRate("whatever");
86 | fail('Should have thrown an error');
87 | } catch(e) { expect(e.message).toBe('The input is NaN') }
88 | try {
89 | pricesStore.setJMDRate(true);
90 | fail('Should have thrown an error');
91 | } catch(e) { expect(e.message).toBe('The input is NaN') }
92 | try {
93 | pricesStore.setJMDRate(false);
94 | fail('Should have thrown an error');
95 | } catch(e) { expect(e.message).toBe('The input is NaN') }
96 | try {
97 | pricesStore.setJMDRate({});
98 | fail('Should have thrown an error');
99 | } catch(e) { expect(e.message).toBe('The input is NaN') }
100 | try {
101 | pricesStore.setJMDRate([]);
102 | fail('Should have thrown an error');
103 | } catch(e) { expect(e.message).toBe('The input is NaN') }
104 | expect(pricesStore.jmd).toBe(0);
105 | });
106 |
107 | it('should be able to reset the store state', () => {
108 | pricesStore.isLoading(true);
109 | pricesStore.setUSDRate(350);
110 | pricesStore.setEURRate(300);
111 | pricesStore.setJMDRate(250);
112 | pricesStore.reset();
113 | expect(pricesStore.usd).toBe(0);
114 | expect(pricesStore.eur).toBe(0);
115 | expect(pricesStore.jmd).toBe(0);
116 | expect(pricesStore.loading).toBe(false);
117 | });
118 | });
--------------------------------------------------------------------------------
/src/components/views/ConfirmTransaction/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ActivityIndicator, Image, StyleSheet, Text, View } from 'react-native';
3 | import { inject, observer } from 'mobx-react';
4 | import { Button } from '@components/widgets';
5 | import { measures } from '@common/styles';
6 | import { Recents as RecentsActions, Transactions as TransactionActions } from '@common/actions';
7 | import { Image as ImageUtils, Transaction as TransactionUtils, Wallet as WalletUtils } from '@common/utils';
8 | import ErrorMessage from './ErrorMessage';
9 | import SuccessMessage from './SuccessMessage';
10 |
11 | @inject('prices', 'wallet')
12 | @observer
13 | export class ConfirmTransaction extends React.Component {
14 |
15 | static navigationOptions = { title: 'Confirm transaction' };
16 |
17 | state = { txn: null, error: null };
18 |
19 | get returnButton() {
20 | return { title: 'Return to wallet', action: () => this.onPressReturn() };
21 | }
22 |
23 | get confirmButton() {
24 | return { title: 'Confirm & send', action: () => this.onPressSend() };
25 | }
26 |
27 | get actionButton() {
28 | if (this.props.wallet.loading) return ;
29 | const buttonConfig = ((this.state.txn && this.state.txn.hash) || this.state.error) ?
30 | this.returnButton : this.confirmButton;
31 | return ;
32 | }
33 |
34 | get estimatedFee() {
35 | const estimate = WalletUtils.estimateFee(this.state.txn);
36 | return WalletUtils.formatBalance(estimate);
37 | }
38 |
39 | get fiatAmount() {
40 | const { txn } = this.state;
41 | return Number(this.props.prices.usd * Number(WalletUtils.formatBalance(txn.value))).toFixed(2);
42 | }
43 |
44 | get fiatEstimatedFee() {
45 | return Number(this.props.prices.usd * Number(this.estimatedFee)).toFixed(2);
46 | }
47 |
48 | componentDidMount() {
49 | const { address, amount } = this.props.navigation.state.params;
50 | const txn = TransactionUtils.createTransaction(address, amount);
51 | this.setState({ txn });
52 | }
53 |
54 | async onPressSend() {
55 | const { wallet } = this.props;
56 | wallet.isLoading(true);
57 | try {
58 | const txn = await TransactionActions.sendTransaction(wallet.item, this.state.txn);
59 | this.setState({ txn });
60 | RecentsActions.saveAddressToRecents(txn.to);
61 | } catch (error) {
62 | this.setState({ error });
63 | } finally {
64 | wallet.isLoading(false);
65 | }
66 | }
67 |
68 | onPressReturn() {
69 | const { wallet } = this.props;
70 | this.props.navigation.navigate('WalletDetails', { wallet: wallet.item, replaceRoute: true, leave: 2 });
71 | }
72 |
73 | render() {
74 | const { error, txn } = this.state;
75 | return (!txn) ? null : (
76 |
77 |
78 |
79 |
80 | Wallet address
81 |
85 |
86 |
88 |
89 |
90 | Amount (ETH)
91 | {WalletUtils.formatBalance(txn.value)} (US$ {this.fiatAmount})
92 |
93 |
94 | Estimated fee (ETH)
95 | {this.estimatedFee} (US$ {this.fiatEstimatedFee})
96 |
97 |
98 |
99 |
100 | {this.actionButton}
101 |
102 | );
103 | }
104 | }
105 |
106 | const styles = StyleSheet.create({
107 | container: {
108 | flex: 1,
109 | padding: measures.defaultPadding,
110 | alignItems: 'stretch',
111 | justifyContent: 'space-between'
112 | },
113 | content: {
114 | flex: 1,
115 | alignItems: 'stretch',
116 | justifyContent: 'flex-start'
117 | },
118 | row: {
119 | flexDirection: 'row',
120 | alignItems: 'center',
121 | justifyContent: 'space-between'
122 | },
123 | textColumn: {
124 | marginVertical: measures.defaultMargin
125 | },
126 | title: {
127 | fontSize: measures.fontSizeMedium + 1,
128 | fontWeight: 'bold'
129 | },
130 | value: {
131 | fontSize: measures.fontSizeMedium,
132 | width: 200
133 | },
134 | avatar: {
135 | width: 100,
136 | height: 100
137 | }
138 | });
--------------------------------------------------------------------------------