Skip to content

Commit

Permalink
ADD: LDK wallet (technical release)
Browse files Browse the repository at this point in the history
  • Loading branch information
Overtorment authored Sep 9, 2021
1 parent b9ad4c0 commit a93935a
Show file tree
Hide file tree
Showing 61 changed files with 2,873 additions and 126 deletions.
8 changes: 6 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,19 @@
// disable rules that we want to enforce only for typescript files
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-use-before-define": "off"
},
"overrides": [
{
// enable the rule specifically for TypeScript files
"files": ["*.ts", "*.tsx"],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": ["error"],
"@typescript-eslint/no-var-requires": ["error"],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-use-before-define": ["error", { "variables": false }]
}
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:
emulator-build: 6110076 # tmp fix for https://github.com/ReactiveCircus/android-emulator-runner/issues/160
target: google_apis
avd-name: Pixel_API_29_AOSP
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
script: npm run e2e:release-test || npm run e2e:release-test || npm run e2e:release-test
env:
TRAVIS: 1
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ install:
- echo no | avdmanager create avd --force -n Pixel_API_29_AOSP -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- printf "\nhw.lcd.height=1334\nhw.lcd.width=750\nhw.lcd.density=320\nskin.name=750x1334" >> /home/travis/.android/avd/Pixel_API_29_AOSP.avd/config.ini
- |
EMU_PARAMS="-verbose -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048"
EMU_PARAMS="-verbose -no-snapshot -no-window -camera-back none -camera-front none -partition-size 2047 -selinux permissive -qemu -m 2048"
EMU_COMMAND="emulator"
# This double "sudo" monstrosity is used to have Travis execute the
# emulator with its new group permissions and help preserve the rule
Expand Down
12 changes: 6 additions & 6 deletions BlueComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export const BlueButton = props => {
return (
<TouchableOpacity
style={{
flex: 1,
borderWidth: 0.7,
borderColor: 'transparent',
backgroundColor: backgroundColor,
Expand Down Expand Up @@ -436,13 +435,13 @@ export const BlueListItem = React.memo(props => {
</ListItem.Subtitle>
)}
</ListItem.Content>
<ListItem.Content right>
{props.rightTitle && (
{props.rightTitle && (
<ListItem.Content right>
<ListItem.Title style={props.rightTitleStyle} numberOfLines={0} right>
{props.rightTitle}
</ListItem.Title>
)}
</ListItem.Content>
</ListItem.Content>
)}
{props.isLoading ? (
<ActivityIndicator />
) : (
Expand Down Expand Up @@ -622,7 +621,8 @@ export class is {
}

export const BlueSpacing20 = props => {
return <View {...props} style={{ height: 20, opacity: 0 }} />;
const { horizontal = false } = props;
return <View {...props} style={{ height: horizontal ? 0 : 20, width: horizontal ? 20 : 0, opacity: 0 }} />;
};

export const BlueSpacing10 = props => {
Expand Down
8 changes: 8 additions & 0 deletions Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import WalletsAddMultisigStep2 from './screen/wallets/addMultisigStep2';
import WalletsAddMultisigHelp from './screen/wallets/addMultisigHelp';
import PleaseBackup from './screen/wallets/pleaseBackup';
import PleaseBackupLNDHub from './screen/wallets/pleaseBackupLNDHub';
import PleaseBackupLdk from './screen/wallets/pleaseBackupLdk';
import ImportWallet from './screen/wallets/import';
import WalletDetails from './screen/wallets/details';
import WalletExport from './screen/wallets/export';
Expand Down Expand Up @@ -75,6 +76,8 @@ import ScanLndInvoice from './screen/lnd/scanLndInvoice';
import LappBrowser from './screen/lnd/browser';
import LNDCreateInvoice from './screen/lnd/lndCreateInvoice';
import LNDViewInvoice from './screen/lnd/lndViewInvoice';
import LdkOpenChannel from './screen/lnd/ldkOpenChannel';
import LdkInfo from './screen/lnd/ldkInfo';
import LNDViewAdditionalInvoiceInformation from './screen/lnd/lndViewAdditionalInvoiceInformation';
import LnurlPay from './screen/lnd/lnurlPay';
import LnurlPaySuccess from './screen/lnd/lnurlPaySuccess';
Expand All @@ -83,6 +86,7 @@ import DrawerList from './screen/wallets/drawerList';
import { isDesktop, isTablet } from './blue_modules/environment';
import SettingsPrivacy from './screen/settings/SettingsPrivacy';
import LNDViewAdditionalInvoicePreImage from './screen/lnd/lndViewAdditionalInvoicePreImage';
import LdkViewLogs from './screen/wallets/ldkViewLogs';

const defaultScreenOptions =
Platform.OS === 'ios'
Expand Down Expand Up @@ -120,7 +124,10 @@ const WalletsRoot = () => {
<WalletsStack.Navigator {...(Platform.OS === 'android' ? { screenOptions: defaultScreenOptions } : null)}>
<WalletsStack.Screen name="WalletsList" component={WalletsList} options={WalletsList.navigationOptions(theme)} />
<WalletsStack.Screen name="WalletTransactions" component={WalletTransactions} options={WalletTransactions.navigationOptions(theme)} />
<WalletsStack.Screen name="LdkOpenChannel" component={LdkOpenChannel} options={LdkOpenChannel.navigationOptions(theme)} />
<WalletsStack.Screen name="LdkInfo" component={LdkInfo} options={LdkInfo.navigationOptions(theme)} />
<WalletsStack.Screen name="WalletDetails" component={WalletDetails} options={WalletDetails.navigationOptions(theme)} />
<WalletsStack.Screen name="LdkViewLogs" component={LdkViewLogs} options={LdkViewLogs.navigationOptions(theme)} />
<WalletsStack.Screen name="TransactionDetails" component={TransactionDetails} options={TransactionDetails.navigationOptions(theme)} />
<WalletsStack.Screen name="TransactionStatus" component={TransactionStatus} options={TransactionStatus.navigationOptions(theme)} />
<WalletsStack.Screen name="HodlHodl" component={HodlHodl} options={HodlHodl.navigationOptions(theme)} />
Expand Down Expand Up @@ -197,6 +204,7 @@ const AddWalletRoot = () => {
component={PleaseBackupLNDHub}
options={PleaseBackupLNDHub.navigationOptions(theme)}
/>
<AddWalletStack.Screen name="PleaseBackupLdk" component={PleaseBackupLdk} options={PleaseBackupLdk.navigationOptions(theme)} />
<AddWalletStack.Screen name="ProvideEntropy" component={ProvideEntropy} options={ProvideEntropy.navigationOptions(theme)} />
<AddWalletStack.Screen
name="WalletsAddMultisig"
Expand Down
1 change: 1 addition & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ android {
}

dependencies {
implementation files("../../node_modules/rn-ldk/android/libs/LDK-release.aar")
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation files("../../node_modules/react-native-tor/android/libs/sifir_android.aar")
Expand Down
10 changes: 10 additions & 0 deletions class/app-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
HDSegwitElectrumSeedP2WPKHWallet,
HDAezeedWallet,
MultisigHDWallet,
LightningLdkWallet,
SLIP39SegwitP2SHWallet,
SLIP39LegacyP2PKHWallet,
SLIP39SegwitBech32Wallet,
Expand Down Expand Up @@ -377,6 +378,9 @@ export class AppStorage {
unserializedWallet.passphrase = passphrase;
}

break;
case LightningLdkWallet.type:
unserializedWallet = LightningLdkWallet.fromJson(key);
break;
case SLIP39SegwitP2SHWallet.type:
unserializedWallet = SLIP39SegwitP2SHWallet.fromJson(key);
Expand Down Expand Up @@ -444,6 +448,12 @@ export class AppStorage {
const secret = wallet.getSecret();
const tempWallets = [];

if (wallet.type === LightningLdkWallet.type) {
/** @type {LightningLdkWallet} */
const ldkwallet = wallet;
ldkwallet.stop().catch(alert);
}

for (const value of this.wallets) {
if (value.type === PlaceholderWallet.type) {
continue;
Expand Down
1 change: 1 addition & 0 deletions class/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './wallets/hd-legacy-breadwallet-wallet';
export * from './wallets/hd-legacy-p2pkh-wallet';
export * from './wallets/watch-only-wallet';
export * from './wallets/lightning-custodian-wallet';
export * from './wallets/lightning-ldk-wallet';
export * from './wallets/abstract-hd-wallet';
export * from './wallets/hd-segwit-bech32-wallet';
export * from './wallets/placeholder-wallet';
Expand Down
152 changes: 152 additions & 0 deletions class/synced-async-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import AsyncStorage from '@react-native-async-storage/async-storage';

const SHA256 = require('crypto-js/sha256');
const ENCHEX = require('crypto-js/enc-hex');
const ENCUTF8 = require('crypto-js/enc-utf8');
const AES = require('crypto-js/aes');

export default class SyncedAsyncStorage {
defaultBaseUrl = 'https://bytes-store.herokuapp.com';
encryptionMarker = 'encrypted://';

namespace: string = '';
encryptionKey: string = '';

constructor(entropy: string) {
if (!entropy) throw new Error('entropy not provided');

this.namespace = this.hashIt(this.hashIt('namespace' + entropy));
this.encryptionKey = this.hashIt(this.hashIt('encryption' + entropy));
}

hashIt(arg: string) {
return ENCHEX.stringify(SHA256(arg));
}

encrypt(clearData: string): string {
return this.encryptionMarker + AES.encrypt(clearData, this.encryptionKey).toString();
}

decrypt(encryptedData: string | null, encryptionKey: string | null = null): string {
if (encryptedData === null) return '';
if (!encryptedData.startsWith(this.encryptionMarker)) return encryptedData;
const bytes = AES.decrypt(encryptedData.replace(this.encryptionMarker, ''), encryptionKey || this.encryptionKey);
return bytes.toString(ENCUTF8);
}

static assertEquals(a: any, b: any) {
if (a !== b) throw new Error('Assertion failed that ' + a + ' equals ' + b);
}

static assertNotEquals(a: any, b: any) {
if (a === b) throw new Error('Assertion failed that ' + a + ' NOT equals ' + b);
}

async selftest(): Promise<boolean> {
const clear = 'text line to be encrypted';
const encrypted = this.encrypt(clear);

SyncedAsyncStorage.assertEquals(encrypted.startsWith(this.encryptionMarker), true);
SyncedAsyncStorage.assertNotEquals(clear, encrypted);
const decrypted = this.decrypt(encrypted);
SyncedAsyncStorage.assertEquals(clear, decrypted);

SyncedAsyncStorage.assertEquals(this.decrypt(clear), clear);

SyncedAsyncStorage.assertEquals(
this.decrypt(
'encrypted://U2FsdGVkX19XQWgwS8q5XjQSQ19OmBsNax4k6NZOAsKFhCgw9sJFwb+qVYfqy6X5',
'3a013f391e59daf2f5074fa66652784d17511ea072d7a8329ff9bddf371932ab',
),
'text line to be encrypted',
);

return true;
}

/**
* @param key {string}
* @param value {string}
*
* @return {string} New sequence number from remote
*/
async setItemRemote(key: string, value: string): Promise<string> {
const that = this;
return new Promise(function (resolve, reject) {
fetch(that.defaultBaseUrl + '/namespace/' + that.namespace + '/' + key, {
method: 'POST',
headers: {
Accept: 'text/plain',
'Content-Type': 'text/plain',
},
body: value,
})
.then(async response => {
const text = await response.text();
console.log('saved, seq num:', text);
resolve(text);
})
.catch(reason => reject(reason));
});
}

async setItem(key: string, value: string) {
value = this.encrypt(value);
await AsyncStorage.setItem(this.namespace + '_' + key, value);
const newSeqNum = await this.setItemRemote(key, value);
const localSeqNum = await this.getLocalSeqNum();
if (+localSeqNum > +newSeqNum) {
// some race condition during save happened..?
return;
}
await AsyncStorage.setItem(this.namespace + '_' + 'seqnum', newSeqNum);
}

async getItemRemote(key: string) {
const response = await fetch(this.defaultBaseUrl + '/namespace/' + this.namespace + '/' + key);
return await response.text();
}

async getItem(key: string) {
return this.decrypt(await AsyncStorage.getItem(this.namespace + '_' + key));
}

async getAllKeysRemote(): Promise<string[]> {
const response = await fetch(this.defaultBaseUrl + '/namespacekeys/' + this.namespace);
const text = await response.text();
return text.split(',');
}

async getAllKeys(): Promise<string[]> {
return (await AsyncStorage.getAllKeys())
.filter(key => key.startsWith(this.namespace + '_'))
.map(key => key.replace(this.namespace + '_', ''));
}

async getLocalSeqNum() {
return (await AsyncStorage.getItem(this.namespace + '_' + 'seqnum')) || '0';
}

/**
* Should be called at init.
* Checks remote sequence number, and if remote is ahead - we sync all keys with local storage.
*/
async synchronize() {
const response = await fetch(this.defaultBaseUrl + '/namespaceseq/' + this.namespace);
const remoteSeqNum = (await response.text()) || '0';
const localSeqNum = await this.getLocalSeqNum();
if (+remoteSeqNum > +localSeqNum) {
console.log('remote storage is ahead, need to sync;', +remoteSeqNum, '>', +localSeqNum);

for (const key of await this.getAllKeysRemote()) {
const value = await this.getItemRemote(key);
await AsyncStorage.setItem(this.namespace + '_' + key, value);
console.log('synced', key, 'to', value);
}

await AsyncStorage.setItem(this.namespace + '_' + 'seqnum', remoteSeqNum);
} else {
console.log('storage is up-to-date, no need for sync');
}
}
}
8 changes: 8 additions & 0 deletions class/wallet-gradient.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { HDLegacyElectrumSeedP2PKHWallet } from './wallets/hd-legacy-electrum-se
import { HDSegwitElectrumSeedP2WPKHWallet } from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
import { MultisigHDWallet } from './wallets/multisig-hd-wallet';
import { HDAezeedWallet } from './wallets/hd-aezeed-wallet';
import { LightningLdkWallet } from './wallets/lightning-ldk-wallet';
import { SLIP39LegacyP2PKHWallet, SLIP39SegwitP2SHWallet, SLIP39SegwitBech32Wallet } from './wallets/slip39-wallets';
import { useTheme } from '@react-navigation/native';

Expand All @@ -26,6 +27,7 @@ export default class WalletGradient {
static defaultGradients = ['#B770F6', '#9013FE'];
static lightningCustodianWallet = ['#F1AA07', '#FD7E37'];
static aezeedWallet = ['#8584FF', '#5351FB'];
static ldkWallet = ['#8584FF', '#5351FB'];

static createWallet = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
Expand Down Expand Up @@ -74,6 +76,9 @@ export default class WalletGradient {
case HDAezeedWallet.type:
gradient = WalletGradient.aezeedWallet;
break;
case LightningLdkWallet.type:
gradient = WalletGradient.ldkWallet;
break;
default:
gradient = WalletGradient.defaultGradients;
break;
Expand Down Expand Up @@ -134,6 +139,9 @@ export default class WalletGradient {
case HDAezeedWallet.type:
gradient = WalletGradient.aezeedWallet;
break;
case LightningLdkWallet.type:
gradient = WalletGradient.ldkWallet;
break;
default:
gradient = WalletGradient.defaultGradients;
break;
Expand Down
10 changes: 10 additions & 0 deletions class/wallet-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
PlaceholderWallet,
SLIP39SegwitP2SHWallet,
SLIP39SegwitBech32Wallet,
LightningLdkWallet,
} from '.';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import loc from '../loc';
Expand Down Expand Up @@ -170,6 +171,15 @@ function WalletImport() {

importText = importText.trim();

if (importText.startsWith('ldk://')) {
const ldk = new LightningLdkWallet();
ldk.setSecret(importText);
if (ldk.valid()) {
await ldk.init();
return WalletImport._saveWallet(ldk);
}
}

if (importText.startsWith('6P')) {
const decryptedKey = await bip38.decryptAsync(importText, password);

Expand Down
4 changes: 4 additions & 0 deletions class/wallets/abstract-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,10 @@ export class AbstractWallet {
this._utxoMetadata[`${txid}:${vout}`] = meta;
}

isSegwit() {
return false;
}

/**
* @returns {string} Root derivation path for wallet if any
*/
Expand Down
Loading

0 comments on commit a93935a

Please sign in to comment.