mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-06 03:38:24 +00:00
Implement seed recovery:
commit9163bd479aAuthor: minibits-cash <support@minibits.cash> Date: Fri Dec 8 22:04:52 2023 +0100 Improve lnurl address handling commita8a7840f70Author: minibits-cash <support@minibits.cash> Date: Fri Dec 8 22:00:11 2023 +0100 Improve lnurl address handling commit7f32a15e57Author: minibits-cash <support@minibits.cash> Date: Thu Dec 7 21:48:22 2023 +0100 Beta.4 release for alpha testers commit1c44c15ccdAuthor: minibits-cash <support@minibits.cash> Date: Thu Dec 7 19:37:54 2023 +0100 Upgrade deps commit2c9ab1b71cAuthor: minibits-cash <support@minibits.cash> Date: Thu Dec 7 11:13:43 2023 +0100 Reset cached mints on seed creation commit5fd9e95d2aAuthor: minibits-cash <support@minibits.cash> Date: Wed Dec 6 00:59:09 2023 +0100 Patch cashu-ts error handling commit436c670e7eAuthor: minibits-cash <support@minibits.cash> Date: Wed Dec 6 00:52:09 2023 +0100 Clean unused deps commitf6174bc607Author: minibits-cash <support@minibits.cash> Date: Wed Dec 6 00:51:45 2023 +0100 Remove debug commit868506a14cAuthor: minibits-cash <support@minibits.cash> Date: Wed Dec 6 00:50:33 2023 +0100 Improve receive error handling commite1bf087565Author: minibits-cash <support@minibits.cash> Date: Tue Dec 5 23:06:36 2023 +0100 Final small UX tuning commit875218d648Author: minibits-cash <support@minibits.cash> Date: Tue Dec 5 16:14:34 2023 +0100 Fix QR code error message commitc37b3e5ffeAuthor: minibits-cash <support@minibits.cash> Date: Tue Dec 5 12:53:23 2023 +0100 New beta release v0.1.5 - Off-device seed backup commit95811817faAuthor: minibits-cash <support@minibits.cash> Date: Tue Dec 5 09:35:21 2023 +0100 Align cashu-ts patch commitf61ad99e64Author: minibits-cash <support@minibits.cash> Date: Mon Dec 4 18:46:41 2023 +0100 Recovery - final tweaks commit6fa384c8efAuthor: minibits-cash <support@minibits.cash> Date: Mon Dec 4 00:10:32 2023 +0100 Fix counters in restore. commit50e9887405Author: minibits-cash <support@minibits.cash> Date: Sat Dec 2 23:53:37 2023 +0100 Seed recovery for testing commit00605c49afAuthor: minibits-cash <support@minibits.cash> Date: Tue Nov 28 00:57:08 2023 +0100 SeedRecovery before first tests commitaaefbe0e63Author: minibits-cash <support@minibits.cash> Date: Thu Nov 23 11:46:08 2023 +0100 Recovery screen - wip commitc019f63f48Author: minibits-cash <support@minibits.cash> Date: Mon Nov 20 00:44:35 2023 +0100 Remote backup screen with seed phrase commitada2f36070Author: minibits-cash <support@minibits.cash> Date: Sun Nov 19 22:03:06 2023 +0100 Generate seed
This commit is contained in:
@@ -89,7 +89,8 @@ jobs:
|
||||
echo MINIBITS_SERVER_API_KEY='${{ secrets.MINIBITS_SERVER_API_KEY }}' >> .env
|
||||
echo MINIBITS_SERVER_API_HOST='https://api.minibits.cash' >> .env
|
||||
echo MINIBITS_NIP05_DOMAIN='@minibits.cash' >> .env
|
||||
echo MINIBITS_RELAY_URL='wss://relay.minibits.cash' >> .env
|
||||
echo MINIBITS_RELAY_URL='wss://relay.minibits.cash' >> .env
|
||||
echo MINIBITS_MINT_URL='wss://mint.minibits.cash/Bitcoin' >> .env
|
||||
echo COMMIT='${{ env.COMMIT_HASH }}' >> .env
|
||||
|
||||
# Install dependecies from package.json
|
||||
|
||||
@@ -89,7 +89,8 @@ jobs:
|
||||
echo MINIBITS_SERVER_API_KEY='${{ secrets.MINIBITS_SERVER_API_KEY }}' >> .env
|
||||
echo MINIBITS_SERVER_API_HOST='https://api.minibits.cash' >> .env
|
||||
echo MINIBITS_NIP05_DOMAIN='@minibits.cash' >> .env
|
||||
echo MINIBITS_RELAY_URL='wss://relay.minibits.cash/test' >> .env
|
||||
echo MINIBITS_RELAY_URL='wss://relay.minibits.cash/test' >> .env
|
||||
echo MINIBITS_MINT_URL='wss://mint.minibits.cash/Bitcoin' >> .env
|
||||
echo COMMIT='${{ env.COMMIT_HASH }}' >> .env
|
||||
|
||||
# Install dependecies from package.json
|
||||
|
||||
@@ -102,8 +102,8 @@ android {
|
||||
applicationId "com.minibits_wallet"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 23
|
||||
versionName "0.1.4"
|
||||
versionCode 28
|
||||
versionName "0.1.5"
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a', 'x86_64', 'x86', 'armeabi-v7a'
|
||||
} // react-native-tor build
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
plugins: [
|
||||
plugins: [
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minibits_wallet",
|
||||
"version": "0.1.4-beta.13",
|
||||
"version": "0.1.5-beta.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android:clean": "cd android && ./gradlew clean",
|
||||
@@ -50,13 +50,13 @@
|
||||
"react-native-flash-message": "^0.4.2",
|
||||
"react-native-gesture-handler": "^2.9.0",
|
||||
"react-native-json-tree": "^1.3.0",
|
||||
"react-native-keychain": "^8.1.1",
|
||||
"react-native-keychain": "^8.1.2",
|
||||
"react-native-logs": "^5.0.1",
|
||||
"react-native-mmkv": "^2.8.0",
|
||||
"react-native-mmkv": "^2.11.0",
|
||||
"react-native-modal": "^13.0.1",
|
||||
"react-native-pager-view": "^6.2.0",
|
||||
"react-native-qrcode-svg": "^6.2.0",
|
||||
"react-native-quick-base64": "^2.0.6",
|
||||
"react-native-quick-base64": "^2.0.7",
|
||||
"react-native-quick-crypto": "^0.6.1",
|
||||
"react-native-quick-sqlite": "^8.0.2",
|
||||
"react-native-reanimated": "3.3.0",
|
||||
|
||||
@@ -13,7 +13,7 @@ index af7649e..91a54c4 100644
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/cashubtc/cashu-ts"
|
||||
diff --git a/node_modules/@cashu/cashu-ts/src/CashuMint.ts b/node_modules/@cashu/cashu-ts/src/CashuMint.ts
|
||||
index f6bc3b1..0bfe1dc 100644
|
||||
index f6bc3b1..e260dcf 100644
|
||||
--- a/node_modules/@cashu/cashu-ts/src/CashuMint.ts
|
||||
+++ b/node_modules/@cashu/cashu-ts/src/CashuMint.ts
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
@@ -189,7 +189,15 @@ index f6bc3b1..0bfe1dc 100644
|
||||
endpoint: joinUrls(mintUrl, 'split'),
|
||||
method: 'POST',
|
||||
requestBody: splitPayload
|
||||
@@ -158,7 +188,7 @@ class CashuMint {
|
||||
@@ -149,7 +179,6 @@ class CashuMint {
|
||||
if (!isObj(data) || !Array.isArray(data?.promises)) {
|
||||
throw new Error('bad response');
|
||||
}
|
||||
-
|
||||
return data;
|
||||
}
|
||||
/**
|
||||
@@ -158,7 +187,7 @@ class CashuMint {
|
||||
* @returns split tokens
|
||||
*/
|
||||
async split(splitPayload: SplitPayload): Promise<SplitResponse> {
|
||||
@@ -198,7 +206,7 @@ index f6bc3b1..0bfe1dc 100644
|
||||
}
|
||||
/**
|
||||
* Ask mint to perform a melt operation. This pays a lightning invoice and destroys tokens matching its amount + fees
|
||||
@@ -166,8 +196,13 @@ class CashuMint {
|
||||
@@ -166,8 +195,13 @@ class CashuMint {
|
||||
* @param meltPayload
|
||||
* @returns
|
||||
*/
|
||||
@@ -214,7 +222,7 @@ index f6bc3b1..0bfe1dc 100644
|
||||
endpoint: joinUrls(mintUrl, 'melt'),
|
||||
method: 'POST',
|
||||
requestBody: meltPayload
|
||||
@@ -189,7 +224,7 @@ class CashuMint {
|
||||
@@ -189,7 +223,7 @@ class CashuMint {
|
||||
* @returns
|
||||
*/
|
||||
async melt(meltPayload: MeltPayload): Promise<MeltResponse> {
|
||||
@@ -223,7 +231,7 @@ index f6bc3b1..0bfe1dc 100644
|
||||
}
|
||||
/**
|
||||
* Estimate fees for a given LN invoice
|
||||
@@ -199,9 +234,11 @@ class CashuMint {
|
||||
@@ -199,9 +233,11 @@ class CashuMint {
|
||||
*/
|
||||
public static async checkFees(
|
||||
mintUrl: string,
|
||||
@@ -237,7 +245,7 @@ index f6bc3b1..0bfe1dc 100644
|
||||
endpoint: joinUrls(mintUrl, 'checkfees'),
|
||||
method: 'POST',
|
||||
requestBody: checkfeesPayload
|
||||
@@ -220,7 +257,7 @@ class CashuMint {
|
||||
@@ -220,7 +256,7 @@ class CashuMint {
|
||||
* @returns estimated Fee
|
||||
*/
|
||||
async checkFees(checkfeesPayload: { pr: string }): Promise<{ fee: number }> {
|
||||
@@ -246,7 +254,7 @@ index f6bc3b1..0bfe1dc 100644
|
||||
}
|
||||
/**
|
||||
* Checks if specific proofs have already been redeemed
|
||||
@@ -230,9 +267,11 @@ class CashuMint {
|
||||
@@ -230,9 +266,11 @@ class CashuMint {
|
||||
*/
|
||||
public static async check(
|
||||
mintUrl: string,
|
||||
@@ -260,7 +268,7 @@ index f6bc3b1..0bfe1dc 100644
|
||||
endpoint: joinUrls(mintUrl, 'check'),
|
||||
method: 'POST',
|
||||
requestBody: checkPayload
|
||||
@@ -250,14 +289,16 @@ class CashuMint {
|
||||
@@ -250,14 +288,16 @@ class CashuMint {
|
||||
* @returns redeemed and unredeemed ordered list of booleans
|
||||
*/
|
||||
async check(checkPayload: CheckSpendablePayload): Promise<CheckSpendableResponse> {
|
||||
@@ -281,7 +289,7 @@ index f6bc3b1..0bfe1dc 100644
|
||||
method: 'POST',
|
||||
requestBody: restorePayload
|
||||
diff --git a/node_modules/@cashu/cashu-ts/src/CashuWallet.ts b/node_modules/@cashu/cashu-ts/src/CashuWallet.ts
|
||||
index b7cf354..d0a9af2 100644
|
||||
index b7cf354..38dd081 100644
|
||||
--- a/node_modules/@cashu/cashu-ts/src/CashuWallet.ts
|
||||
+++ b/node_modules/@cashu/cashu-ts/src/CashuWallet.ts
|
||||
@@ -1,7 +1,7 @@
|
||||
@@ -304,7 +312,7 @@ index b7cf354..d0a9af2 100644
|
||||
import {
|
||||
bytesToNumber,
|
||||
cleanToken,
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
@@ -25,8 +25,10 @@ import {
|
||||
getDecodedToken,
|
||||
getDefaultAmountPreference,
|
||||
splitAmount
|
||||
@@ -312,10 +320,39 @@ index b7cf354..d0a9af2 100644
|
||||
-import { deriveBlindingFactor, deriveSecret, deriveSeedFromMnemonic } from './secrets.js';
|
||||
+} from './utils';
|
||||
+import { deriveBlindingFactor, deriveSecret, deriveSeedFromMnemonic } from './secrets';
|
||||
+import { validateMnemonic } from '@scure/bip39';
|
||||
+import { wordlist } from '@scure/bip39/wordlists/english.js';
|
||||
|
||||
/**
|
||||
* Class that represents a Cashu wallet.
|
||||
@@ -69,13 +69,16 @@ class CashuWallet {
|
||||
@@ -43,15 +45,23 @@ class CashuWallet {
|
||||
* @param mint Cashu mint instance is used to make api calls
|
||||
* @param mnemonic mnemonic seed phrase to initial derivation key for this wallets deterministic secrets
|
||||
*/
|
||||
- constructor(mint: CashuMint, keys?: MintKeys, mnemonic?: string) {
|
||||
+ constructor(mint: CashuMint, keys?: MintKeys, mnemonicOrSeed?: string | Uint8Array) {
|
||||
this._keys = keys || {};
|
||||
this.mint = mint;
|
||||
if (keys) {
|
||||
this._keysetId = deriveKeysetId(this._keys);
|
||||
}
|
||||
- if (mnemonic) {
|
||||
- this._seed = deriveSeedFromMnemonic(mnemonic);
|
||||
+ if (!mnemonicOrSeed) {
|
||||
+ return
|
||||
}
|
||||
+ if (mnemonicOrSeed instanceof Uint8Array) {
|
||||
+ this._seed = mnemonicOrSeed
|
||||
+ return
|
||||
+ }
|
||||
+ if (!validateMnemonic(mnemonicOrSeed, wordlist)) {
|
||||
+ throw new Error('Tried to instantiate with mnemonic, but mnemonic was invalid');
|
||||
+ }
|
||||
+ this._seed = deriveSeedFromMnemonic(mnemonicOrSeed);
|
||||
}
|
||||
|
||||
get keys(): MintKeys {
|
||||
@@ -69,13 +79,16 @@ class CashuWallet {
|
||||
* @param proofs (only the 'secret' field is required)
|
||||
* @returns
|
||||
*/
|
||||
@@ -335,7 +372,84 @@ index b7cf354..d0a9af2 100644
|
||||
}
|
||||
/**
|
||||
* Starts a minting process by requesting an invoice from the mint
|
||||
@@ -512,8 +515,8 @@ class CashuWallet {
|
||||
@@ -166,6 +179,7 @@ class CashuWallet {
|
||||
const { token } = cleanToken(getDecodedToken(encodedToken));
|
||||
const tokenEntries: Array<TokenEntry> = [];
|
||||
const tokenEntriesWithError: Array<TokenEntry> = [];
|
||||
+ let errors: Error[] = []
|
||||
let newKeys: MintKeys | undefined;
|
||||
for (const tokenEntry of token) {
|
||||
if (!tokenEntry?.proofs?.length) {
|
||||
@@ -175,8 +189,12 @@ class CashuWallet {
|
||||
const {
|
||||
proofsWithError,
|
||||
proofs,
|
||||
- newKeys: newKeysFromReceive
|
||||
+ newKeys: newKeysFromReceive,
|
||||
+ error
|
||||
} = await this.receiveTokenEntry(tokenEntry, preference, count);
|
||||
+
|
||||
+ if(error) {errors.push(error)}
|
||||
+
|
||||
if (proofsWithError?.length) {
|
||||
tokenEntriesWithError.push(tokenEntry);
|
||||
continue;
|
||||
@@ -185,15 +203,16 @@ class CashuWallet {
|
||||
if (!newKeys) {
|
||||
newKeys = newKeysFromReceive;
|
||||
}
|
||||
- } catch (error) {
|
||||
- console.error(error);
|
||||
+ } catch (e: any) {
|
||||
+ errors.push(e)
|
||||
tokenEntriesWithError.push(tokenEntry);
|
||||
}
|
||||
}
|
||||
return {
|
||||
token: { token: tokenEntries },
|
||||
tokensWithErrors: tokenEntriesWithError.length ? { token: tokenEntriesWithError } : undefined,
|
||||
- newKeys
|
||||
+ newKeys,
|
||||
+ errors: errors.length ? errors : undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,6 +231,7 @@ class CashuWallet {
|
||||
const proofsWithError: Array<Proof> = [];
|
||||
const proofs: Array<Proof> = [];
|
||||
let newKeys: MintKeys | undefined;
|
||||
+ let err: Error | undefined = undefined
|
||||
try {
|
||||
const amount = tokenEntry.proofs.reduce((total, curr) => total + curr.amount, 0);
|
||||
if (!preference) {
|
||||
@@ -224,6 +244,7 @@ class CashuWallet {
|
||||
count
|
||||
);
|
||||
const { promises, error } = await CashuMint.split(tokenEntry.mint, payload);
|
||||
+ if(error) {err = {name: 'Error', message: error}}
|
||||
const newProofs = dhke.constructProofs(
|
||||
promises,
|
||||
blindedMessages.rs,
|
||||
@@ -235,14 +256,15 @@ class CashuWallet {
|
||||
tokenEntry.mint === this.mint.mintUrl
|
||||
? await this.changedKeys([...(promises || [])])
|
||||
: undefined;
|
||||
- } catch (error) {
|
||||
- console.error(error);
|
||||
+ } catch (e: any) {
|
||||
+ err = e
|
||||
proofsWithError.push(...tokenEntry.proofs);
|
||||
}
|
||||
return {
|
||||
proofs,
|
||||
proofsWithError: proofsWithError.length ? proofsWithError : undefined,
|
||||
- newKeys
|
||||
+ newKeys,
|
||||
+ error: err
|
||||
};
|
||||
}
|
||||
|
||||
@@ -512,8 +534,8 @@ class CashuWallet {
|
||||
const secrets: Array<Uint8Array> = [];
|
||||
const rs: Array<bigint> = [];
|
||||
for (let i = 0; i < amounts.length; i++) {
|
||||
@@ -363,7 +477,7 @@ index ca37559..02843b5 100644
|
||||
import { bytesToHex } from '@noble/curves/abstract/utils';
|
||||
|
||||
diff --git a/node_modules/@cashu/cashu-ts/src/index.ts b/node_modules/@cashu/cashu-ts/src/index.ts
|
||||
index b1f2c00..138c251 100644
|
||||
index b1f2c00..0c0f85c 100644
|
||||
--- a/node_modules/@cashu/cashu-ts/src/index.ts
|
||||
+++ b/node_modules/@cashu/cashu-ts/src/index.ts
|
||||
@@ -1,10 +1,10 @@
|
||||
@@ -378,16 +492,42 @@ index b1f2c00..138c251 100644
|
||||
import { decode as getDecodedLnInvoice } from '@gandlaf21/bolt11-decode';
|
||||
-import { generateNewMnemonic } from './secrets.js';
|
||||
-export * from './model/types/index.js';
|
||||
+import { generateNewMnemonic } from './secrets';
|
||||
+import { generateNewMnemonic, deriveSeedFromMnemonic } from './secrets';
|
||||
+export * from './model/types/index';
|
||||
|
||||
export {
|
||||
CashuMint,
|
||||
@@ -14,5 +14,6 @@ export {
|
||||
deriveKeysetId,
|
||||
getDecodedLnInvoice,
|
||||
setGlobalRequestOptions,
|
||||
- generateNewMnemonic
|
||||
+ generateNewMnemonic,
|
||||
+ deriveSeedFromMnemonic
|
||||
};
|
||||
diff --git a/node_modules/@cashu/cashu-ts/src/model/types/index.ts b/node_modules/@cashu/cashu-ts/src/model/types/index.ts
|
||||
index 4855c33..4546acc 100644
|
||||
index 4855c33..91e45bb 100644
|
||||
--- a/node_modules/@cashu/cashu-ts/src/model/types/index.ts
|
||||
+++ b/node_modules/@cashu/cashu-ts/src/model/types/index.ts
|
||||
@@ -215,6 +215,7 @@ export type CheckSpendableResponse = {
|
||||
@@ -41,6 +41,8 @@ export type ReceiveTokenEntryResponse = {
|
||||
* If the mint has rotated keys, this field will be populated with the new keys.
|
||||
*/
|
||||
newKeys?: MintKeys;
|
||||
+
|
||||
+ error?: Error
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,6 +78,8 @@ export type ReceiveResponse = {
|
||||
* If the mint has rotated keys, this field will be populated with the new keys.
|
||||
*/
|
||||
newKeys?: MintKeys;
|
||||
+
|
||||
+ errors?: Error[] | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -215,6 +219,7 @@ export type CheckSpendableResponse = {
|
||||
* Ordered list for checked proofs. True if the secret has not been redeemed at the mint before
|
||||
*/
|
||||
spendable: Array<boolean>;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
export const HeaderBg = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1440" height="200" preserveAspectRatio="none" viewBox="0 0 1440 200">
|
||||
<g mask="url("#SvgjsMask1228")" fill="none">
|
||||
<rect width="1440" height="200" x="0" y="0" fill="rgba(33, 108, 233, 1)"></rect>
|
||||
<path d="M747.1057075216023 127.75562642682651L754.3970814083749 75.87480543830213 702.5162604198505 68.58343155152957 695.2248865330779 120.46425254005393z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M327.2048426729167 35.928073236888665L288.5226793723616 68.38626219552378 320.9808683309967 107.06842549607889 359.6630316315518 74.61023653744377z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M692.4246915017623 170.19979034423497L702.9325329152182 215.7142519174232 748.4469944884064 205.20641050396728 737.9391530749505 159.69194893077903z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M1011.3353816446419 62.94159037130783L1028.581667963285 89.49854242930147 1055.1386200212787 72.25225611065815 1037.8923337026356 45.69530405266451z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M969.6723418652923 55.983154930508455L919.0428285294618 52.44279447268238 915.5024680716357 103.07230780851287 966.1319814074662 106.61266826633894z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M490.49547511700763 73.50446578850527L517.250567607621 35.29423377783492 479.04033559695057 8.53914128722161 452.28524310633725 46.749373297891964z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M1421.5534289524064 76.40745393189856L1402.143414506712 126.97227030992588 1452.7082308847394 146.38228475562033 1472.1182453304336 95.817468377593z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M44.18649852640779 56.50055120620612L12.331357360316396 43.63023874742407-0.5389550984656495 75.48537991351547 31.316186067625747 88.35569237229751z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M567.477420162059 5.581617787479144L553.758592711916 33.709382410470965 581.8863573349078 47.42820986061403 595.6051847850508 19.300445237622203z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M1191.7568875108875 151.89571627870086L1189.2314299670188 103.70711568508801 1141.042829373406 106.23257322895671 1143.5682869172747 154.42117382256956z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M1233.0041486652651 43.54078493335189L1218.5612249444973 13.928402945024839 1188.9488429561702 28.371326665792672 1203.391766676938 57.983708654119724z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M1093.9611587853271 54.44612369420207L1125.8106675944764 85.20283683431656 1156.567380734591 53.353328025167215 1124.7178719254416 22.596614885052723z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M1431.181750298965 152.22148179381813L1388.2730376608383 138.27959591769297 1374.3311517847133 181.18830855581956 1417.23986442284 195.13019443194472z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M689.945851702067 69.68986125874905L724.3502661613595 91.18812542474701 745.8485303273575 56.783710965454546 711.444115868065 35.28544679945657z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M592.5530077850946 131.1723799971202L580.8661884602851 167.1407114350277 616.8345198981926 178.8275307598371 628.5213392230021 142.8591993219296z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M1066.3984237341463 93.45412929443842L1015.8249975827404 76.04030217184369 998.4111704601456 126.6137283232496 1048.9845966115515 144.0275554458443z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M520.0887716328363 213.80147367701232L526.5041910090813 161.55207579613258 474.2547931282015 155.13665641988757 467.8393737519565 207.38605430076734z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M1209.4433647244202 151.13012700405633L1208.364068011502 182.03714104259996 1239.2710820500456 183.11643775551815 1240.3503787629638 152.20942371697453z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M1412.5831611707363 137.77032976422595L1425.2780766404287 104.69894429691828 1392.206691173121 92.00402882722595 1379.5117757034286 125.07541429453362z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M200.67441576045212 107.21742873144655L188.93683921376643 134.8694262079934 216.5888366903133 146.6070027546791 228.32641323699897 118.95500527813222z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<mask id="SvgjsMask1228">
|
||||
<rect width="1440" height="200" fill="#ffffff"></rect>
|
||||
</mask>
|
||||
<style>
|
||||
@keyframes float1 {
|
||||
0%{transform: translate(0, 0)}
|
||||
50%{transform: translate(-10px, 0)}
|
||||
100%{transform: translate(0, 0)}
|
||||
}
|
||||
|
||||
.triangle-float1 {
|
||||
animation: float1 5s infinite;
|
||||
}
|
||||
|
||||
@keyframes float2 {
|
||||
0%{transform: translate(0, 0)}
|
||||
50%{transform: translate(-5px, -5px)}
|
||||
100%{transform: translate(0, 0)}
|
||||
}
|
||||
|
||||
.triangle-float2 {
|
||||
animation: float2 4s infinite;
|
||||
}
|
||||
|
||||
@keyframes float3 {
|
||||
0%{transform: translate(0, 0)}
|
||||
50%{transform: translate(0, -10px)}
|
||||
100%{transform: translate(0, 0)}
|
||||
}
|
||||
|
||||
.triangle-float3 {
|
||||
animation: float3 6s infinite;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
</svg>`
|
||||
|
||||
|
||||
export const HeaderBg2 = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1440" height="200" preserveAspectRatio="none" viewBox="0 0 1440 200">
|
||||
<g mask="url("#SvgjsMask1105")" fill="none">
|
||||
<rect width="1440" height="200" x="0" y="0" fill="rgba(33, 108, 233, 1)"></rect>
|
||||
<path d="M749.0831241863771 121.38806886260159L754.6023483557941 68.8761586102141 702.0904381034065 63.356934440797104 696.5712139339896 115.86884469318458z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M1351.3054843130171 54.0624429919247L1354.7515332124415 103.3432382069029 1404.0323284274198 99.8971893074785 1400.5862795279954 50.61639409250029z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M94.16973276472656 153.7220346920555L121.01408311325078 123.9083632425251 91.20041166372037 97.06401289400088 64.35606131519616 126.87768434353129z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M466.97021985019103 150.27038846783466L442.0906957238391 124.50688704231337 416.32719429831775 149.38641116866532 441.2067184246697 175.14991259418662z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M589.7063339756562 117.83984879717408L627.6221439178643 106.24782238548573 616.030117506176 68.33201244327765 578.1143075639678 79.924038854966z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M918.9888996274942 100.97281796775246L943.7668434439643 71.44361440737686 914.2376398835886 46.665670590906856 889.4596960671186 76.19487415128246z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M362.0235285710007 60.29738945472211L362.56413622240916 29.325997848238075 331.5927446159251 28.785390196829653 331.0521369645167 59.756781803313686z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M921.2410433137273 123.30434229262224L898.4513471220594 148.61486409224395 923.7618689216811 171.40456028391196 946.551565113349 146.09403848429025z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M305.1087778387738 165.28226340185944L287.71314790346094 133.8997162655259 256.3306007671274 151.29534620083876 273.72623070244026 182.6778933371723z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M1274.054443780785 60.90011888152908L1242.8063434543035 78.221223733202 1260.1274483059765 109.46932405968349 1291.375548632458 92.14821920801057z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M768.4138023884847 183.50966038245656L791.7112207622134 215.57580582570978 823.7773662054667 192.278387451981 800.4799478317378 160.21224200872777z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M955.206998076348 162.76877783195323L999.3710378176617 140.26607563127058 976.8683356169789 96.10203588995692 932.7042958756654 118.60473809063959z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M12.809281352625778 38.32420067506579L15.333160449379674 86.4826827035264 63.49164247784029 83.9588036067725 60.967763381086385 35.800321578311895z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M730.8203243804758 169.9399636519063L759.0370257813572 126.49005377027117 715.587115899722 98.27335236938977 687.3704144988407 141.72326225102492z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M829.0504139631389 113.90023900242355L838.0171519845464 164.75313732450644 888.8700503066293 155.78639930309896 879.9033122852218 104.93350098101605z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
<path d="M1113.9957280681033 78.02099446221344L1140.6850821326325 57.16898575084809 1119.8330734212673 30.47963168631881 1093.1437193567378 51.33164039768416z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M172.06045733524795 59.181689102757716L206.81411742545455 105.30135375782231 252.93378208051914 70.5476936676157 218.18012199031253 24.428029012551107z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M562.8672114116416 114.31024483273946L600.7862711130508 138.93517012974428 625.4111964100557 101.01611042833508 587.4921367086465 76.39118513133025z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float3"></path>
|
||||
<path d="M888.0288011006094 114.15671294879479L858.1621970497167 92.4573549387241 836.462839039646 122.32395898961676 866.3294430905387 144.02331699968744z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float2"></path>
|
||||
<path d="M1126.2173189168125 54.03684274720831L1105.9951016004316 26.203348441102374 1078.1616072943254 46.42556575748341 1098.3838246107066 74.25906006358935z" fill="rgba(89, 157, 82, 0.8)" class="triangle-float1"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<mask id="SvgjsMask1105">
|
||||
<rect width="1440" height="200" fill="#ffffff"></rect>
|
||||
</mask>
|
||||
<style>
|
||||
@keyframes float1 {
|
||||
0%{transform: translate(0, 0)}
|
||||
50%{transform: translate(-10px, 0)}
|
||||
100%{transform: translate(0, 0)}
|
||||
}
|
||||
|
||||
.triangle-float1 {
|
||||
animation: float1 5s infinite;
|
||||
}
|
||||
|
||||
@keyframes float2 {
|
||||
0%{transform: translate(0, 0)}
|
||||
50%{transform: translate(-5px, -5px)}
|
||||
100%{transform: translate(0, 0)}
|
||||
}
|
||||
|
||||
.triangle-float2 {
|
||||
animation: float2 4s infinite;
|
||||
}
|
||||
|
||||
@keyframes float3 {
|
||||
0%{transform: translate(0, 0)}
|
||||
50%{transform: translate(0, -10px)}
|
||||
100%{transform: translate(0, 0)}
|
||||
}
|
||||
|
||||
.triangle-float3 {
|
||||
animation: float3 6s infinite;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
</svg>`
|
||||
@@ -67,6 +67,7 @@ import { faKey } from '@fortawesome/free-solid-svg-icons/faKey'
|
||||
import { faCircleNodes } from '@fortawesome/free-solid-svg-icons/faCircleNodes'
|
||||
import { faBullseye } from '@fortawesome/free-solid-svg-icons/faBullseye'
|
||||
import { faEyeSlash } from '@fortawesome/free-solid-svg-icons/faEyeSlash'
|
||||
import { faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons/faUpRightFromSquare'
|
||||
|
||||
|
||||
export type IconTypes = keyof typeof iconRegistry
|
||||
@@ -123,7 +124,8 @@ export const iconRegistry = {
|
||||
faKey: faKey,
|
||||
faCircleNodes: faCircleNodes,
|
||||
faBullseye: faBullseye,
|
||||
faEyeSlash: faEyeSlash,
|
||||
faEyeSlash: faEyeSlash,
|
||||
faUpRightFromSquare: faUpRightFromSquare,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export const InfoModal: FC<InfoModalProps> = function ({ message }) {
|
||||
onBackdropPress={() => onClose()}
|
||||
onBackButtonPress={() => onClose()}
|
||||
ContentComponent={
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginRight: spacing.medium}}>
|
||||
<View style={{ padding: spacing.small, flexDirection: 'row', alignItems: 'center', marginRight: spacing.medium}}>
|
||||
<Icon icon="faInfoCircle" size={spacing.large} color="white" />
|
||||
<Text style={{ marginHorizontal: spacing.extraSmall}}>{message}</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import { StyleSheet, View, ActivityIndicator, ViewProps } from 'react-native'
|
||||
import { useThemeColor } from '../theme'
|
||||
import { Text } from './Text'
|
||||
|
||||
export function Loading(props: ViewProps) {
|
||||
export function Loading(props: ViewProps & {statusMessage?: string}) {
|
||||
return (
|
||||
<View style={[{
|
||||
flex: 1,
|
||||
@@ -11,8 +12,9 @@ export function Loading(props: ViewProps) {
|
||||
opacity: 0.5,
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
}, StyleSheet.absoluteFillObject]}>
|
||||
}, StyleSheet.absoluteFillObject, props.style]}>
|
||||
<ActivityIndicator color="#ccc" animating size="large" />
|
||||
{props.statusMessage && (<Text style={{opacity: 1}} text={props.statusMessage}/>)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -14,3 +14,4 @@ export * from "./InfoModal"
|
||||
export * from "./BottomModal"
|
||||
export * from "./Loading"
|
||||
export * from "./ScanIcon"
|
||||
export * from "./HeaderBg"
|
||||
|
||||
+3
-1
@@ -59,7 +59,7 @@ const en = {
|
||||
other: "{{count}} mints have been added to your wallet",
|
||||
},
|
||||
toReceive: "Amount to receive",
|
||||
received: "Amount received:",
|
||||
received: "Amount received",
|
||||
sharePaymentRequest: "Send payment request",
|
||||
sharePaymentRequestDescription: "Send payment request to one of your contacts, so that the payer can pay you exact amount.",
|
||||
scanToReceive: "Scan or paste to receive",
|
||||
@@ -156,6 +156,8 @@ const en = {
|
||||
backupScreen: {
|
||||
localBackup: "Local backup",
|
||||
localBackupDescription: "Local backup stores a copy of all your ecash in the local database. Ecash is never deleted unless you switch off the backup. Please note that this backup is not encrypted.",
|
||||
remoteBackup: "Off-device backup",
|
||||
remoteBackupDescription: "Seed phrase allows you to recover your ecash balance in case of device loss.",
|
||||
recoveryTool: "Recovery tool",
|
||||
recoveryToolDescription: "Show ecash backed up in the local database and attempt to recover unspent ones into the wallet in case the wallet storage gets corrupted or a transaction fails due to an unexpected error.",
|
||||
removeSpentCoins: "Remove spent ecash",
|
||||
|
||||
+47
-4
@@ -1,7 +1,9 @@
|
||||
import {Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree'
|
||||
import {cast, Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree'
|
||||
import {withSetPropAction} from './helpers/withSetPropAction'
|
||||
import type {MintKeys} from '@cashu/cashu-ts/dist/lib/es5/model/types'
|
||||
import {colors, getRandomIconColor} from '../theme'
|
||||
import { log } from '../services'
|
||||
import { deriveKeysetId } from '@cashu/cashu-ts'
|
||||
|
||||
// used as a helper type across app
|
||||
export type MintBalance = {
|
||||
@@ -14,6 +16,11 @@ export enum MintStatus {
|
||||
OFFLINE = 'OFFLINE'
|
||||
}
|
||||
|
||||
export type MintProofsCounter = {
|
||||
keyset: string
|
||||
counter: number
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a Cashu mint
|
||||
*/
|
||||
@@ -24,11 +31,23 @@ export const MintModel = types
|
||||
shortname: types.maybe(types.string),
|
||||
keys: types.frozen<MintKeys>(),
|
||||
keysets: types.frozen<string[]>(),
|
||||
proofsCounters: types.array(
|
||||
types.model('MintProofsCounter', {
|
||||
keyset: types.string,
|
||||
counter: types.number,
|
||||
})
|
||||
),
|
||||
color: types.optional(types.string, colors.palette.iconBlue200),
|
||||
status: types.optional(types.frozen<MintStatus>(), MintStatus.ONLINE),
|
||||
createdAt: types.optional(types.Date, new Date()),
|
||||
})
|
||||
.actions(withSetPropAction)
|
||||
.views(self => ({
|
||||
get currentProofsCounter() {
|
||||
const currentKeyset = deriveKeysetId(self.keys)
|
||||
return self.proofsCounters.find(c => c.keyset === currentKeyset)
|
||||
},
|
||||
}))
|
||||
.actions(self => ({
|
||||
validateURL(url: string) {
|
||||
try {
|
||||
@@ -61,12 +80,36 @@ export const MintModel = types
|
||||
setStatus(status: MintStatus) {
|
||||
self.status = status
|
||||
},
|
||||
updateKeys(keyset: string, keys: MintKeys) {
|
||||
// not sure why to keep keysetIDs history and even keys, cashu-ts seems to (on first look) get current keys before each mint interaction
|
||||
updateKeys(keyset: string, keys: MintKeys) {
|
||||
self.keysets.push(keyset)
|
||||
self.keys = keys
|
||||
self.keysets = cast(self.keysets)
|
||||
},
|
||||
}))
|
||||
increaseProofsCounter(numberOfProofs: number) {
|
||||
const currentCounter = self.currentProofsCounter
|
||||
|
||||
if (currentCounter) {
|
||||
log.trace('[increaseProofsCounter]', 'Before update', {currentCounter})
|
||||
currentCounter.counter += numberOfProofs
|
||||
log.trace('[increaseProofsCounter]', 'Updated proofsCounter', {numberOfProofs, currentCounter})
|
||||
} else {
|
||||
// If the counter doesn't exist, create a new one
|
||||
const currentKeyset = deriveKeysetId(self.keys)
|
||||
|
||||
const newCounter = {
|
||||
keyset: currentKeyset,
|
||||
counter: numberOfProofs,
|
||||
}
|
||||
|
||||
self.proofsCounters.push(newCounter)
|
||||
|
||||
log.trace('[increaseProofsCounter]', 'Adding new proofsCounter', {newCounter})
|
||||
}
|
||||
// Make sure to cast the frozen array back to a mutable array
|
||||
self.proofsCounters = cast(self.proofsCounters)
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
export type Mint = {
|
||||
mintUrl: string
|
||||
|
||||
@@ -77,6 +77,18 @@ export const MintsStoreModel = types
|
||||
self.blockedMintUrls.remove(blockedMint.mintUrl)
|
||||
log.debug('[unblockMint]', 'Mint unblocked in MintsStore')
|
||||
},
|
||||
increaseProofsCounter(mintUrl: string, numberOfProofs: number) {
|
||||
const mintInstance = self.findByUrl(mintUrl)
|
||||
|
||||
if(mintInstance) {
|
||||
mintInstance.increaseProofsCounter(numberOfProofs)
|
||||
return mintInstance.currentProofsCounter
|
||||
}
|
||||
|
||||
log.warn('[increaseProofsCounter]', 'Could not find mint', {mintUrl})
|
||||
return 0
|
||||
|
||||
},
|
||||
}))
|
||||
.views(self => ({
|
||||
get mintCount() {
|
||||
@@ -91,14 +103,14 @@ export const MintsStoreModel = types
|
||||
self.mints.forEach((mint: Mint) => {
|
||||
const {hostname} = mint
|
||||
|
||||
if (!grouped[hostname]) {
|
||||
grouped[hostname] = {
|
||||
hostname,
|
||||
mints: [],
|
||||
}
|
||||
if (!grouped[hostname as string]) {
|
||||
grouped[hostname as string] = {
|
||||
hostname,
|
||||
mints: [],
|
||||
}
|
||||
}
|
||||
|
||||
grouped[hostname].mints.push(mint)
|
||||
grouped[hostname as string].mints.push(mint)
|
||||
})
|
||||
|
||||
return Object.values(grouped) as MintsByHostname[]
|
||||
@@ -123,6 +135,16 @@ export const MintsStoreModel = types
|
||||
}
|
||||
return missingMints
|
||||
},
|
||||
currentProofsCounterValue(mintUrl: string) {
|
||||
const mintInstance = self.findByUrl(mintUrl)
|
||||
|
||||
if (mintInstance) {
|
||||
return mintInstance.currentProofsCounter?.counter || 0
|
||||
}
|
||||
|
||||
log.warn('[currentProofsCounter]', 'Could not find mint', {mintUrl})
|
||||
return 0
|
||||
},
|
||||
}))
|
||||
|
||||
export interface MintsStore extends Instance<typeof MintsStoreModel> {}
|
||||
|
||||
@@ -62,10 +62,11 @@ export const ProofsStoreModel = types
|
||||
},
|
||||
}))
|
||||
.actions(self => ({
|
||||
addProofs(newProofs: Proof[], isPending: boolean = false): number {
|
||||
addProofs(newProofs: Proof[], isPending: boolean = false): {addedAmount: number, addedProofs: Proof[]} {
|
||||
try {
|
||||
const proofs = isPending ? self.pendingProofs : self.proofs
|
||||
let addedAmount: number = 0
|
||||
let addedProofs: Proof[] = []
|
||||
|
||||
for (const proof of newProofs) {
|
||||
if(self.alreadyExists(proof)) {
|
||||
@@ -81,18 +82,23 @@ export const ProofsStoreModel = types
|
||||
}
|
||||
|
||||
addedAmount += proof.amount
|
||||
addedProofs.push(proof)
|
||||
}
|
||||
|
||||
log.debug('[addProofs]', `Added new ${newProofs.length}${isPending ? ' pending' : ''} proofs to the ProofsStore`,)
|
||||
// Handle counter increment
|
||||
const mintsStore = getRootStore(self).mintsStore
|
||||
mintsStore.increaseProofsCounter(newProofs[0].mintUrl as string, addedProofs.length)
|
||||
|
||||
log.debug('[addProofs]', `Added new ${addedProofs.length}${isPending ? ' pending' : ''} proofs to the ProofsStore`,)
|
||||
|
||||
const rootStore = getRootStore(self)
|
||||
const {userSettingsStore} = rootStore
|
||||
|
||||
if (userSettingsStore.isLocalBackupOn === true) {
|
||||
Database.addOrUpdateProofs(newProofs, isPending) // isSpent = false
|
||||
if (userSettingsStore.isLocalBackupOn === true && addedProofs.length > 0) {
|
||||
Database.addOrUpdateProofs(addedProofs, isPending) // isSpent = false
|
||||
}
|
||||
|
||||
return addedAmount
|
||||
return { addedAmount, addedProofs }
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.STORAGE_ERROR, e.message)
|
||||
}
|
||||
@@ -204,6 +210,14 @@ export const ProofsStoreModel = types
|
||||
},
|
||||
}))
|
||||
.views(self => ({
|
||||
getMintBalance: (mintUrl: string) => {
|
||||
const balances = self.getBalances().mintBalances
|
||||
|
||||
const mintBalance = balances
|
||||
.find((balance: MintBalance) => balance.mint === mintUrl)
|
||||
|
||||
return mintBalance
|
||||
},
|
||||
getMintBalancesWithEnoughBalance: (amount: number) => {
|
||||
const balances = self.getBalances().mintBalances
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import React from "react"
|
||||
import Config from "../config"
|
||||
import {
|
||||
WelcomeScreen,
|
||||
RemoteRecoveryScreen,
|
||||
MintsScreen
|
||||
} from "../screens"
|
||||
import { useStores } from "../models"
|
||||
import { TabsNavigator, TabsParamList } from "./TabsNavigator"
|
||||
@@ -35,6 +37,8 @@ import { navigationRef, useBackButtonHandler } from "./navigationUtilities"
|
||||
*/
|
||||
export type AppStackParamList = {
|
||||
Welcome: undefined
|
||||
RemoteRecovery: undefined
|
||||
Mints: undefined
|
||||
Tabs: NavigatorScreenParams<TabsParamList>
|
||||
}
|
||||
|
||||
@@ -68,6 +72,8 @@ const AppStack = observer(function AppStack() {
|
||||
) : (
|
||||
<>
|
||||
<Stack.Screen name="Welcome" component={WelcomeScreen} />
|
||||
<Stack.Screen name="RemoteRecovery" component={RemoteRecoveryScreen} />
|
||||
<Stack.Screen name="Mints" component={MintsScreen} />
|
||||
<Stack.Screen name="Tabs" component={TabsNavigator}/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
PrivacyScreen,
|
||||
UpdateScreen,
|
||||
BackupScreen,
|
||||
RemoteBackupScreen,
|
||||
LocalRecoveryScreen,
|
||||
RelaysScreen,
|
||||
TranDetailScreen,
|
||||
@@ -124,7 +125,7 @@ export type WalletStackParamList = {
|
||||
TranDetail: {id: number}
|
||||
TranHistory: undefined
|
||||
PaymentRequests: undefined
|
||||
Transfer: {encodedInvoice?: string, paymentRequest?: PaymentRequest, lnurlParams?: LNURLPayParams, paymentOption?: SendOption,}
|
||||
Transfer: {encodedInvoice?: string, paymentRequest?: PaymentRequest, lnurlParams?: LNURLPayParams & {address?: string}, paymentOption?: SendOption,}
|
||||
Topup: {contact?: Contact, relays?: string[], paymentOption?: ReceiveOption, lnurlParams?: LNURLWithdrawParams}
|
||||
ContactsNavigator: {screen: string, params: any}
|
||||
SettingsNavigator: {screen: string, params: any}
|
||||
@@ -212,6 +213,7 @@ export type SettingsStackParamList = {
|
||||
updateSize: string,
|
||||
}
|
||||
Backup: undefined
|
||||
RemoteBackup: undefined
|
||||
LocalRecovery: undefined
|
||||
Developer: undefined
|
||||
Relays: undefined
|
||||
@@ -240,6 +242,7 @@ const SettingsNavigator = function SettingsNavigator() {
|
||||
<SettingsStack.Screen name="Privacy" component={PrivacyScreen} />
|
||||
<SettingsStack.Screen name="Update" component={UpdateScreen} />
|
||||
<SettingsStack.Screen name="Backup" component={BackupScreen} />
|
||||
<SettingsStack.Screen name="RemoteBackup" component={RemoteBackupScreen} />
|
||||
<SettingsStack.Screen name="LocalRecovery" component={LocalRecoveryScreen} />
|
||||
<SettingsStack.Screen name="Developer" component={DeveloperScreen} />
|
||||
<SettingsStack.Screen name="Relays" component={RelaysScreen} />
|
||||
|
||||
@@ -75,6 +75,10 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
|
||||
navigation.navigate('LocalRecovery')
|
||||
}
|
||||
|
||||
const gotoRemoteBackup = function () {
|
||||
navigation.navigate('RemoteBackup')
|
||||
}
|
||||
|
||||
const checkSpent = async function () {
|
||||
setIsLoading(true)
|
||||
const result = (await Wallet.checkSpent()) as {
|
||||
@@ -106,60 +110,77 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
|
||||
<Text preset="heading" text="Backup" style={{color: 'white'}} />
|
||||
</View>
|
||||
<View style={$contentContainer}>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
tx="backupScreen.localBackup"
|
||||
subTx="backupScreen.localBackupDescription"
|
||||
leftIcon='faDownload'
|
||||
leftIconColor={
|
||||
isLocalBackupOn
|
||||
? colors.palette.success200
|
||||
: colors.palette.neutral400
|
||||
}
|
||||
leftIconInverse={true}
|
||||
RightComponent={
|
||||
<View style={$rightContainer}>
|
||||
<Switch
|
||||
onValueChange={toggleBackupSwitch}
|
||||
value={isLocalBackupOn}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
style={$item}
|
||||
/>
|
||||
{isLocalBackupOn && (
|
||||
<ListItem
|
||||
tx="backupScreen.recoveryTool"
|
||||
subTx="backupScreen.recoveryToolDescription"
|
||||
leftIcon='faUpload'
|
||||
leftIconColor={colors.palette.focus300}
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
tx="backupScreen.remoteBackup"
|
||||
subTx="backupScreen.remoteBackupDescription"
|
||||
leftIcon='faUpRightFromSquare'
|
||||
leftIconColor={colors.palette.blue200}
|
||||
leftIconInverse={true}
|
||||
style={$item}
|
||||
onPress={gotoLocalRecovery}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
tx="backupScreen.removeSpentCoins"
|
||||
subTx="backupScreen.removeSpentCoinsDescription"
|
||||
leftIcon='faRecycle'
|
||||
leftIconColor={colors.palette.secondary300}
|
||||
leftIconInverse={true}
|
||||
style={$item}
|
||||
onPress={checkSpent}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
onPress={gotoRemoteBackup}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
tx="backupScreen.localBackup"
|
||||
subTx="backupScreen.localBackupDescription"
|
||||
leftIcon='faDownload'
|
||||
leftIconColor={
|
||||
isLocalBackupOn
|
||||
? colors.palette.success200
|
||||
: colors.palette.neutral400
|
||||
}
|
||||
leftIconInverse={true}
|
||||
RightComponent={
|
||||
<View style={$rightContainer}>
|
||||
<Switch
|
||||
onValueChange={toggleBackupSwitch}
|
||||
value={isLocalBackupOn}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
style={$item}
|
||||
/>
|
||||
{isLocalBackupOn && (
|
||||
<ListItem
|
||||
tx="backupScreen.recoveryTool"
|
||||
subTx="backupScreen.recoveryToolDescription"
|
||||
leftIcon='faUpload'
|
||||
leftIconColor={colors.palette.focus300}
|
||||
leftIconInverse={true}
|
||||
style={$item}
|
||||
onPress={gotoLocalRecovery}
|
||||
topSeparator={true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
tx="backupScreen.removeSpentCoins"
|
||||
subTx="backupScreen.removeSpentCoinsDescription"
|
||||
leftIcon='faRecycle'
|
||||
leftIconColor={colors.palette.secondary300}
|
||||
leftIconInverse={true}
|
||||
style={$item}
|
||||
onPress={checkSpent}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{isLoading && <Loading />}
|
||||
</View>
|
||||
<BottomModal
|
||||
|
||||
@@ -6,12 +6,11 @@ import {colors, spacing, typography, useThemeColor} from '../theme'
|
||||
import {Header, Icon, Screen} from '../components'
|
||||
import {useStores} from '../models'
|
||||
import {ContactsStackScreenProps} from '../navigation'
|
||||
import { NostrClient, NostrUnsignedEvent } from '../services'
|
||||
import { NostrClient } from '../services'
|
||||
import { PrivateContacts } from './Contacts/PrivateContacts'
|
||||
import { PublicContacts } from './Contacts/PublicContacts'
|
||||
import { log } from '../services/logService'
|
||||
import { getImageSource } from '../utils/utils'
|
||||
import { MINIBITS_NIP05_DOMAIN } from '@env'
|
||||
import { ReceiveOption } from './ReceiveOptionsScreen'
|
||||
import { SendOption } from './SendOptionsScreen'
|
||||
|
||||
@@ -55,7 +54,7 @@ export const ContactsScreen: FC<ContactsScreenProps> = observer(function Contact
|
||||
const renderScene = ({route}: {route: Route}) => {
|
||||
switch (route.key) {
|
||||
case 'first':
|
||||
return <PrivateContacts navigation={navigation} paymentOption={paymentOption} />
|
||||
return <PrivateContacts navigation={navigation} paymentOption={paymentOption} />
|
||||
case 'second':
|
||||
return <PublicContacts navigation={navigation} paymentOption={paymentOption} />
|
||||
default:
|
||||
|
||||
@@ -117,23 +117,27 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
|
||||
{
|
||||
text: 'Confirm',
|
||||
onPress: async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Delete database
|
||||
Database.cleanAll()
|
||||
// Delete Nostr keys
|
||||
await KeyChain.removeNostrKeypair()
|
||||
// Delete Encryption key
|
||||
await KeyChain.removeMmkvEncryptionKey()
|
||||
// Clean mobx storage
|
||||
MMKVStorage.clearAll()
|
||||
// recreate db schema
|
||||
Database.getInstance()
|
||||
setIsLoading(false)
|
||||
setInfo('Factory reset completed, please restart immediately.')
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Delete database
|
||||
Database.cleanAll()
|
||||
// Delete Nostr keys
|
||||
await KeyChain.removeNostrKeypair()
|
||||
// Delete Encryption key
|
||||
await KeyChain.removeMmkvEncryptionKey()
|
||||
// Delete mnemonic
|
||||
await KeyChain.removeMnemonic()
|
||||
// Delete seed
|
||||
await KeyChain.removeSeed()
|
||||
// Clean mobx storage
|
||||
MMKVStorage.clearAll()
|
||||
// recreate db schema
|
||||
Database.getInstance()
|
||||
setIsLoading(false)
|
||||
setInfo('Factory reset completed, please restart immediately.')
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,8 +2,11 @@ import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useCallback, useRef, useState} from 'react'
|
||||
import {Alert, TextInput, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {
|
||||
MINIBITS_MINT_URL
|
||||
} from '@env'
|
||||
import {spacing, typography, useThemeColor, colors} from '../theme'
|
||||
import {SettingsStackScreenProps} from '../navigation'
|
||||
import {AppStackScreenProps, SettingsStackScreenProps} from '../navigation'
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
@@ -19,12 +22,12 @@ import {
|
||||
import {Mint} from '../models/Mint'
|
||||
import {useStores} from '../models'
|
||||
import {useHeader} from '../utils/useHeader'
|
||||
import {MintKeys, MintKeySets, MintClient} from '../services'
|
||||
import {MintKeys, MintClient} from '../services'
|
||||
import {log} from '../services/logService'
|
||||
import AppError from '../utils/AppError'
|
||||
import {translate} from '../i18n'
|
||||
import {MintListItem} from './Mints/MintListItem'
|
||||
import { infoMessage } from '../utils/utils'
|
||||
|
||||
|
||||
|
||||
export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(function MintsScreen({route, navigation}) {
|
||||
@@ -37,6 +40,7 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
|
||||
const mintInputRef = useRef<TextInput>(null)
|
||||
|
||||
const [mintUrl, setMintUrl] = useState('')
|
||||
const [defaultMintUrl, setDefaultMintUrl] = useState<string>(MINIBITS_MINT_URL)
|
||||
const [selectedMint, setSelectedMint] = useState<Mint | undefined>()
|
||||
const [info, setInfo] = useState('')
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
@@ -117,6 +121,12 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
|
||||
}
|
||||
|
||||
|
||||
const addDefaultMint = async function () {
|
||||
setMintUrl(defaultMintUrl)
|
||||
toggleAddMintModal()
|
||||
}
|
||||
|
||||
|
||||
const removeMint = async function () {
|
||||
if (!selectedMint) {return}
|
||||
|
||||
@@ -129,8 +139,6 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
|
||||
|
||||
message += 'Do you really want to remove this mint from the wallet?'
|
||||
|
||||
|
||||
|
||||
Alert.alert(
|
||||
'Warning',
|
||||
message,
|
||||
@@ -220,6 +228,7 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
|
||||
<Card
|
||||
style={$actionCard}
|
||||
ContentComponent={
|
||||
<>
|
||||
<ListItem
|
||||
text={'Add mint'}
|
||||
LeftComponent={<Icon
|
||||
@@ -232,6 +241,22 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
|
||||
style={$actionItem}
|
||||
onPress={toggleAddMintModal}
|
||||
/>
|
||||
{!mintsStore.alreadyExists(defaultMintUrl) && (
|
||||
<ListItem
|
||||
text={'Add Minibits mint'}
|
||||
LeftComponent={<Icon
|
||||
containerStyle={$iconContainer}
|
||||
icon="faCoins"
|
||||
size={spacing.medium}
|
||||
color={iconColor}
|
||||
/>
|
||||
}
|
||||
style={$actionItem}
|
||||
onPress={addDefaultMint}
|
||||
topSeparator={true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{mintsStore.mintCount > 0 && (
|
||||
@@ -313,6 +338,7 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
|
||||
bottomSeparator={true}
|
||||
style={{paddingHorizontal: spacing.medium}}
|
||||
/>
|
||||
<Text text={`Current recovery index: ${selectedMint?.currentProofsCounter?.counter}`} size='xxs' style={{alignSelf: 'center', marginTop: spacing.small}} />
|
||||
</View>
|
||||
}
|
||||
onBackButtonPress={onMintUnselect}
|
||||
@@ -404,8 +430,9 @@ const $contentContainer: TextStyle = {
|
||||
|
||||
const $actionCard: ViewStyle = {
|
||||
marginBottom: spacing.small,
|
||||
marginTop: -spacing.extraLarge * 1.5,
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
minHeight: 70,
|
||||
paddingVertical: 0,
|
||||
}
|
||||
|
||||
const $actionItem: ViewStyle = {
|
||||
@@ -414,8 +441,8 @@ const $actionItem: ViewStyle = {
|
||||
}
|
||||
|
||||
const $card: ViewStyle = {
|
||||
marginBottom: spacing.small,
|
||||
paddingTop: 0,
|
||||
marginBottom: spacing.small,
|
||||
// paddingTop: 0,
|
||||
}
|
||||
|
||||
const $cardHeading: TextStyle = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useState, useCallback} from 'react'
|
||||
import React, {FC, useState, useCallback, useEffect} from 'react'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {Alert, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
@@ -19,8 +19,6 @@ import {useHeader} from '../utils/useHeader'
|
||||
import {log} from '../services/logService'
|
||||
import AppError from '../utils/AppError'
|
||||
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
||||
import { infoMessage } from '../utils/utils'
|
||||
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
|
||||
|
||||
export enum ReceiveOption {
|
||||
SEND_PAYMENT_REQUEST = 'SEND_PAYMENT_REQUEST',
|
||||
@@ -36,12 +34,12 @@ export const ReceiveOptionsScreen: FC<WalletStackScreenProps<'ReceiveOptions'>>
|
||||
onLeftPress: () => navigation.goBack(),
|
||||
})
|
||||
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
|
||||
const [info, setInfo] = useState('')
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
|
||||
|
||||
const gotoContacts = function () {
|
||||
navigation.navigate('ContactsNavigator', {
|
||||
screen: 'Contacts',
|
||||
|
||||
@@ -132,7 +132,8 @@ export const ReceiveScreen: FC<WalletStackScreenProps<'Receive'>> = observer(
|
||||
if (error) {
|
||||
setResultModalInfo({
|
||||
status,
|
||||
message: error.message,
|
||||
title: error.message || 'Receive failed',
|
||||
message: JSON.parse(error.params).message || error.message,
|
||||
})
|
||||
} else {
|
||||
setResultModalInfo({
|
||||
@@ -290,12 +291,14 @@ export const ReceiveScreen: FC<WalletStackScreenProps<'Receive'>> = observer(
|
||||
}
|
||||
/>
|
||||
{transactionStatus === TransactionStatus.COMPLETED ? (
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.close'}
|
||||
onPress={gotoWallet}
|
||||
/>
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.close'}
|
||||
onPress={gotoWallet}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={$bottomContainer}>
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useEffect, useState} from 'react'
|
||||
import {FlatList, Linking, Platform, Switch, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import {validateMnemonic} from '@scure/bip39'
|
||||
import { wordlist } from '@scure/bip39/wordlists/english'
|
||||
import {colors, spacing, useThemeColor} from '../theme'
|
||||
import {SettingsStackScreenProps} from '../navigation' // @demo remove-current-line
|
||||
import {
|
||||
Icon,
|
||||
ListItem,
|
||||
Screen,
|
||||
Text,
|
||||
Card,
|
||||
Loading,
|
||||
ErrorModal,
|
||||
InfoModal,
|
||||
BottomModal,
|
||||
Button,
|
||||
$sizeStyles,
|
||||
} from '../components'
|
||||
import {useHeader} from '../utils/useHeader'
|
||||
import AppError, { Err } from '../utils/AppError'
|
||||
import { KeyChain, log, MintClient } from '../services'
|
||||
import { scale } from '@gocodingnow/rn-size-matters'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
|
||||
|
||||
export const RemoteBackupScreen: FC<SettingsStackScreenProps<'RemoteBackup'>> = observer(function RemoteBackupScreen(_props) {
|
||||
const {navigation, route} = _props
|
||||
useHeader({
|
||||
leftIcon: 'faArrowLeft',
|
||||
onLeftPress: () => {
|
||||
navigation.goBack()
|
||||
},
|
||||
})
|
||||
|
||||
// const {userSettingsStore} = useStores()
|
||||
|
||||
const [info, setInfo] = useState('')
|
||||
const [mnemonic, setMnemonic] = useState<string>()
|
||||
const [mnemonicArray, setMnemonicArray] = useState<string[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isNewMnemonic, setIsNewMnemonic] = useState(false)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
const getmnemonic = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
let mnemonic: string | undefined = undefined
|
||||
|
||||
mnemonic = await MintClient.getMnemonic()
|
||||
|
||||
if(!mnemonic) {
|
||||
// wallets upgraded from 0.1.4 with no generated seed
|
||||
mnemonic = await MintClient.getOrCreateMnemonic() // expensive, derives seed
|
||||
MintClient.resetCachedWallets() // force all cached wallet instances to be recreated with seed
|
||||
setIsNewMnemonic(true)
|
||||
}
|
||||
|
||||
if (!validateMnemonic(mnemonic, wordlist)) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Corrupted or invalid mnemonic. Please move funds and reinstall this wallet.')
|
||||
}
|
||||
|
||||
setMnemonic(mnemonic)
|
||||
setMnemonicArray(mnemonic.split(/\s+/))
|
||||
setIsLoading(false)
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
getmnemonic()
|
||||
}, [])
|
||||
|
||||
|
||||
const onCopy = function (): void {
|
||||
try {
|
||||
if(mnemonic) {
|
||||
Clipboard.setString(mnemonic)
|
||||
|
||||
if(isNewMnemonic) {
|
||||
setInfo('To apply backup to your existing funds, send your whole balance to yourself. Otherwise only funds from now on can be restored.')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic.')
|
||||
} catch (e: any) {
|
||||
setInfo(`Could not copy: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = function (e: AppError): void {
|
||||
setIsLoading(false)
|
||||
setError(e)
|
||||
}
|
||||
|
||||
const headerBg = useThemeColor('header')
|
||||
|
||||
return (
|
||||
<Screen style={$screen}>
|
||||
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
||||
<Text preset="heading" text="Off-device backup" style={{color: 'white'}} />
|
||||
</View>
|
||||
<View style={$contentContainer}>
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text='Your mnemonic phrase'
|
||||
subText='These 12 words sequence allows you to recover your ecash balance in case of device loss. Keep them in safe place.'
|
||||
leftIcon='faInfoCircle'
|
||||
leftIconColor={colors.palette.iconYellow300}
|
||||
leftIconInverse={true}
|
||||
style={$item}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<>
|
||||
{isLoading && <Loading />}
|
||||
<FlatList
|
||||
data={mnemonicArray}
|
||||
numColumns={2}
|
||||
renderItem={({ item, index }) => {
|
||||
return(
|
||||
<Button
|
||||
key={index}
|
||||
preset={'secondary'}
|
||||
onPress={() => false}
|
||||
text={`${index + 1}. ${item}`}
|
||||
style={{minWidth: scale(150), margin: spacing.tiny, minHeight: scale(25)}}
|
||||
textStyle={[$sizeStyles.xs, {padding: 0, margin: 0, lineHeight: 16}]}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
keyExtractor={(item) => item}
|
||||
style={{ flexGrow: 0 }}
|
||||
contentContainerStyle={{alignItems: 'center'}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
FooterComponent={
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="default"
|
||||
style={{margin: spacing.small}}
|
||||
text="Copy"
|
||||
onPress={onCopy}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
{error && <ErrorModal error={error} />}
|
||||
{info && <InfoModal message={info} />}
|
||||
</Screen>
|
||||
)
|
||||
})
|
||||
|
||||
const $screen: ViewStyle = {}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
alignItems: 'center',
|
||||
padding: spacing.medium,
|
||||
height: spacing.screenHeight * 0.18,
|
||||
}
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
padding: spacing.extraSmall,
|
||||
}
|
||||
|
||||
const $card: ViewStyle = {
|
||||
marginBottom: spacing.small,
|
||||
}
|
||||
|
||||
const $buttonContainer: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
}
|
||||
|
||||
const $bottomModal: ViewStyle = {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.large,
|
||||
paddingHorizontal: spacing.small,
|
||||
}
|
||||
|
||||
const $item: ViewStyle = {
|
||||
paddingHorizontal: spacing.small,
|
||||
paddingLeft: 0,
|
||||
}
|
||||
|
||||
const $rightContainer: ViewStyle = {
|
||||
// padding: spacing.extraSmall,
|
||||
alignSelf: 'center',
|
||||
//marginLeft: spacing.small,
|
||||
}
|
||||
@@ -0,0 +1,804 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useEffect, useRef, useState} from 'react'
|
||||
import {LayoutAnimation, Platform, Switch, TextInput, TextStyle, UIManager, View, ViewStyle} from 'react-native'
|
||||
import {validateMnemonic} from '@scure/bip39'
|
||||
import { wordlist } from '@scure/bip39/wordlists/english'
|
||||
import {colors, spacing, useThemeColor} from '../theme'
|
||||
import {AppStackScreenProps} from '../navigation' // @demo remove-current-line
|
||||
import {
|
||||
Icon,
|
||||
ListItem,
|
||||
Screen,
|
||||
Text,
|
||||
Card,
|
||||
Loading,
|
||||
ErrorModal,
|
||||
InfoModal,
|
||||
BottomModal,
|
||||
Button,
|
||||
$sizeStyles,
|
||||
} from '../components'
|
||||
import {useHeader} from '../utils/useHeader'
|
||||
import AppError, { Err } from '../utils/AppError'
|
||||
import { KeyChain, log, MintClient, MintKeys } from '../services'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import { useStores } from '../models'
|
||||
import { MintListItem } from './Mints/MintListItem'
|
||||
import { Mint } from '../models/Mint'
|
||||
import { CashuMint, deriveKeysetId } from '@cashu/cashu-ts'
|
||||
import { CashuUtils } from '../services/cashu/cashuUtils'
|
||||
import { Proof } from '../models/Proof'
|
||||
import {
|
||||
type Proof as CashuProof,
|
||||
} from '@cashu/cashu-ts'
|
||||
import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../models/Transaction'
|
||||
import { ResultModalInfo } from './Wallet/ResultModalInfo'
|
||||
import { deriveSeedFromMnemonic } from '@cashu/cashu-ts'
|
||||
|
||||
if (Platform.OS === 'android' &&
|
||||
UIManager.setLayoutAnimationEnabledExperimental) {
|
||||
UIManager.setLayoutAnimationEnabledExperimental(true)
|
||||
}
|
||||
|
||||
const RESTORE_INDEX_INTERVAL = 50
|
||||
|
||||
export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = observer(function RemoteRecoveryScreen(_props) {
|
||||
const {navigation, route} = _props
|
||||
useHeader({
|
||||
leftIcon: 'faArrowLeft',
|
||||
onLeftPress: () => {
|
||||
navigation.goBack()
|
||||
},
|
||||
})
|
||||
|
||||
const {mintsStore, proofsStore, userSettingsStore, transactionsStore} = useStores()
|
||||
const mnemonicInputRef = useRef<TextInput>(null)
|
||||
|
||||
const [info, setInfo] = useState('')
|
||||
const [mnemonic, setMnemonic] = useState<string>('')
|
||||
const [mnemonicExists, setMnemonicExists] = useState(false)
|
||||
const [isValidMnemonic, setIsValidMnemonic] = useState(false)
|
||||
const [seed, setSeed] = useState<Uint8Array>()
|
||||
const [startIndex, setStartIndex] = useState<number>(0) // start of interval of indexes of proofs to recover
|
||||
const [endIndex, setEndIndex] = useState<number>(RESTORE_INDEX_INTERVAL) // end of interval
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [isErrorsModalVisible, setIsErrorsModalVisible] = useState(false)
|
||||
const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus, message: string} | undefined>()
|
||||
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
|
||||
const [lastRecoveredAmount, setLastRecoveredAmount] = useState<number>(0)
|
||||
const [recoveryErrors, setRecoveryErrors] = useState<AppError[]>([])
|
||||
const [statusMessage, setStatusMessage] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
const getMnemonic = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const existing = await MintClient.getMnemonic()
|
||||
|
||||
if(existing) {
|
||||
setMnemonicExists(true)
|
||||
}
|
||||
setIsLoading(false)
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
getMnemonic()
|
||||
}, [])
|
||||
|
||||
|
||||
const toggleResultModal = () => {
|
||||
if(isResultModalVisible === true) {
|
||||
setResultModalInfo(undefined)
|
||||
}
|
||||
setIsResultModalVisible(previousState => !previousState)
|
||||
}
|
||||
|
||||
|
||||
const toggleErrorsModal = () => {
|
||||
setIsErrorsModalVisible(previousState => !previousState)
|
||||
}
|
||||
|
||||
|
||||
const onPaste = async function () {
|
||||
try {
|
||||
const maybeMnemonic = await Clipboard.getString()
|
||||
|
||||
if(!maybeMnemonic) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic phrase.')
|
||||
}
|
||||
|
||||
setMnemonic(maybeMnemonic)
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onConfirm = async function () {
|
||||
try {
|
||||
setStatusMessage('Deriving seed, this takes a while...')
|
||||
|
||||
if(!mnemonic) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic.')
|
||||
}
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
setIsLoading(true)
|
||||
|
||||
if (!validateMnemonic(mnemonic, wordlist)) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Invalid mnemonic phrase. Provide 12 words sequence separated by blank spaces.')
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const binarySeed = deriveSeedFromMnemonic(mnemonic) // expensive
|
||||
setSeed(binarySeed)
|
||||
setIsValidMnemonic(true)
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
setIsLoading(false)
|
||||
}, 200)
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
const onBack = function (): void {
|
||||
return navigation.goBack()
|
||||
}
|
||||
|
||||
|
||||
const onAddMints = function (): void {
|
||||
return navigation.navigate('Mints')
|
||||
}
|
||||
|
||||
|
||||
const startRecovery = async function () {
|
||||
setStatusMessage('Starting recovery...')
|
||||
setIsLoading(true)
|
||||
setTimeout(() => doRecovery(), 200)
|
||||
}
|
||||
|
||||
const doRecovery = async function () {
|
||||
let errors: AppError[] = []
|
||||
let recoveredAmount: number = 0
|
||||
let alreadySpentAmount: number = 0
|
||||
|
||||
setStatusMessage('Loading mints...')
|
||||
|
||||
for (const mint of mintsStore.allMints) {
|
||||
const transactionData: TransactionData[] = []
|
||||
let transactionId: number = 0
|
||||
|
||||
const pendingTransactionData: TransactionData[] = []
|
||||
let pendingTransactionId: number = 0
|
||||
|
||||
try {
|
||||
// TODO allow input or get previous keysets from mint and try to restore from them
|
||||
setStatusMessage(`Restoring from ${mint.hostname}...`)
|
||||
log.info('[restore]', `Restoring from ${mint.hostname}...`)
|
||||
|
||||
const { proofs, newKeys } = await MintClient.restore(
|
||||
mint.mintUrl,
|
||||
startIndex,
|
||||
endIndex,
|
||||
seed as Uint8Array
|
||||
)
|
||||
|
||||
log.debug('[restore]', `Restored proofs`, proofs.length)
|
||||
setStatusMessage(`Found ${proofs.length} proofs...`)
|
||||
|
||||
// exit if nothing recovered
|
||||
if (proofs.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
// need to move counter by whole interval to avoid duplicate _B!!!
|
||||
mintsStore.increaseProofsCounter(mint.mintUrl, Math.abs(endIndex - startIndex))
|
||||
|
||||
if(newKeys) {updateMintKeys(mint.mintUrl as string, newKeys)}
|
||||
|
||||
const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint(
|
||||
mint.mintUrl,
|
||||
proofs as Proof[]
|
||||
)
|
||||
|
||||
log.debug('[restore]', `Spent and pending proofs`, {spent: spent.length, pending: pending.length})
|
||||
|
||||
setStatusMessage(`${spent.length} proofs were already spent...`)
|
||||
|
||||
const spentAmount = CashuUtils.getProofsAmount(spent as Proof[])
|
||||
alreadySpentAmount += spentAmount
|
||||
|
||||
const unspent = proofs.filter(proof => !spent.includes(proof))
|
||||
|
||||
if(unspent && unspent.length > 0) {
|
||||
|
||||
setStatusMessage(`Completing recovery...`)
|
||||
|
||||
const amount = CashuUtils.getProofsAmount(unspent as Proof[])
|
||||
recoveredAmount += amount
|
||||
|
||||
// Let's create new draft receive transaction in database
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PREPARED,
|
||||
amount,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
const newTransaction: Transaction = {
|
||||
type: TransactionType.RECEIVE,
|
||||
amount,
|
||||
data: JSON.stringify(transactionData),
|
||||
memo: 'Wallet recovery',
|
||||
mint: mint.mintUrl,
|
||||
status: TransactionStatus.PREPARED,
|
||||
}
|
||||
|
||||
const draftTransaction: TransactionRecord = await transactionsStore.addTransaction(newTransaction)
|
||||
transactionId = draftTransaction.id as number
|
||||
|
||||
const { amountToAdd, addedAmount } = addCashuProofs(
|
||||
unspent,
|
||||
mint.mintUrl,
|
||||
transactionId as number
|
||||
)
|
||||
|
||||
if (amountToAdd !== addedAmount) {
|
||||
await transactionsStore.updateReceivedAmount(
|
||||
transactionId as number,
|
||||
addedAmount,
|
||||
)
|
||||
|
||||
recoveredAmount = recoveredAmount - amount + addedAmount
|
||||
}
|
||||
|
||||
// Finally, update completed transaction
|
||||
transactionData.push({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
await transactionsStore.updateStatus(
|
||||
transactionId,
|
||||
TransactionStatus.COMPLETED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
const balanceAfter = proofsStore.getBalances().totalBalance
|
||||
await transactionsStore.updateBalanceAfter(transactionId, balanceAfter)
|
||||
}
|
||||
|
||||
if(pending && pending.length > 0) {
|
||||
|
||||
// infoMessage(`Found pending ecash...`)
|
||||
setStatusMessage(`Found ${pending.length} pending proofs...`)
|
||||
log.debug(`Found pending ecash with ${mint.hostname}...`)
|
||||
|
||||
const amount = CashuUtils.getProofsAmount(pending as Proof[])
|
||||
|
||||
// Let's create new draft receive transaction in database
|
||||
pendingTransactionData.push({
|
||||
status: TransactionStatus.PREPARED,
|
||||
amount,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
const newTransaction: Transaction = {
|
||||
type: TransactionType.RECEIVE,
|
||||
amount,
|
||||
data: JSON.stringify(transactionData),
|
||||
memo: 'Wallet recovery - pending',
|
||||
mint: mint.mintUrl,
|
||||
status: TransactionStatus.PREPARED,
|
||||
}
|
||||
|
||||
const draftTransaction: TransactionRecord = await transactionsStore.addTransaction(newTransaction)
|
||||
pendingTransactionId = draftTransaction.id as number
|
||||
|
||||
const { amountToAdd, addedAmount } = addCashuProofs(
|
||||
pending,
|
||||
mint.mintUrl,
|
||||
pendingTransactionId as number,
|
||||
true // isPending = true
|
||||
)
|
||||
|
||||
if (amountToAdd !== addedAmount) {
|
||||
await transactionsStore.updateReceivedAmount(
|
||||
transactionId as number,
|
||||
addedAmount,
|
||||
)
|
||||
}
|
||||
|
||||
// Finally, update pending transaction
|
||||
pendingTransactionData.push({
|
||||
status: TransactionStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
await transactionsStore.updateStatus(
|
||||
pendingTransactionId,
|
||||
TransactionStatus.PENDING,
|
||||
JSON.stringify(pendingTransactionData),
|
||||
)
|
||||
}
|
||||
|
||||
} catch(e: any) {
|
||||
|
||||
if (mint) {
|
||||
e.params = {mintUrl: mint.mintUrl}
|
||||
}
|
||||
|
||||
// log.error(e, {mintUrl: mint.mintUrl})
|
||||
errors.push(e)
|
||||
|
||||
if (transactionId > 0) {
|
||||
transactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: formatError(e),
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
await transactionsStore.updateStatus(
|
||||
transactionId,
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
}
|
||||
|
||||
if (pendingTransactionId > 0) {
|
||||
pendingTransactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: formatError(e),
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
await transactionsStore.updateStatus(
|
||||
pendingTransactionId,
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(pendingTransactionData),
|
||||
)
|
||||
}
|
||||
setStatusMessage(undefined)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
setLastRecoveredAmount(recoveredAmount)
|
||||
setStartIndex(startIndex + RESTORE_INDEX_INTERVAL)
|
||||
setEndIndex(endIndex + RESTORE_INDEX_INTERVAL)
|
||||
setStatusMessage(undefined)
|
||||
setIsLoading(false)
|
||||
|
||||
if(recoveredAmount > 0) {
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
message: `${recoveredAmount} sats were recovered into your wallet.`
|
||||
})
|
||||
} else {
|
||||
if(errors.length > 0) {
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.ERROR,
|
||||
message: `Recovery ended up with errors.`
|
||||
})
|
||||
setRecoveryErrors(errors)
|
||||
} else {
|
||||
if(alreadySpentAmount > 0) {
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.EXPIRED,
|
||||
message: `Good news is that already spent ecash has been found. Continue with next recovery interval.`
|
||||
})
|
||||
} else {
|
||||
setResultModalInfo({
|
||||
status: TransactionStatus.EXPIRED,
|
||||
message: `Nothing has been found in this recovery interval.`
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
toggleResultModal() // open
|
||||
|
||||
}
|
||||
|
||||
|
||||
// TODO: make it DRY with walletService
|
||||
const updateMintKeys = function (mintUrl: string, newKeys: MintKeys) {
|
||||
if(!CashuUtils.validateMintKeys(newKeys)) {
|
||||
// silent
|
||||
log.warn('[_updateMintKeys]', 'Invalid mint keys to update, skipping', newKeys)
|
||||
return
|
||||
}
|
||||
|
||||
const keyset = deriveKeysetId(newKeys)
|
||||
const mint = mintsStore.findByUrl(mintUrl)
|
||||
|
||||
return mint?.updateKeys(keyset, newKeys) // TODO make only newKeys as param
|
||||
}
|
||||
|
||||
|
||||
// TODO: make it DRY with walletService
|
||||
const addCashuProofs = function (
|
||||
proofsToAdd: CashuProof[],
|
||||
mintUrl: string,
|
||||
transactionId: number,
|
||||
isPending: boolean = false
|
||||
): {
|
||||
amountToAdd: number,
|
||||
addedAmount: number,
|
||||
addedProofs: Proof[]
|
||||
} {
|
||||
// Add internal references
|
||||
for (const proof of proofsToAdd as Proof[]) {
|
||||
proof.tId = transactionId
|
||||
proof.mintUrl = mintUrl
|
||||
}
|
||||
|
||||
const amountToAdd = CashuUtils.getProofsAmount(proofsToAdd as Proof[])
|
||||
// Creates proper model instances and adds them to the wallet
|
||||
const {addedAmount, addedProofs} = proofsStore.addProofs(proofsToAdd as Proof[], isPending)
|
||||
|
||||
log.trace('[addCashuProofs]', 'Added recovered proofs to the wallet with amount', { amountToAdd, addedAmount, isPending })
|
||||
|
||||
return {
|
||||
amountToAdd,
|
||||
addedAmount,
|
||||
addedProofs
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make it DRY with walletService
|
||||
const formatError = function (e: AppError) {
|
||||
return {
|
||||
name: e.name,
|
||||
message: e.message.slice(0, 100),
|
||||
params: e.params || {},
|
||||
} as AppError
|
||||
}
|
||||
|
||||
|
||||
const onComplete = async () => {
|
||||
try {
|
||||
if(!seed || !mnemonic) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic or seed.')
|
||||
}
|
||||
|
||||
await KeyChain.saveMnemonic(mnemonic)
|
||||
await KeyChain.saveSeed(seed as Uint8Array)
|
||||
|
||||
userSettingsStore.setIsOnboarded(true)
|
||||
navigation.navigate('Tabs')
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleError = function (e: AppError): void {
|
||||
setIsLoading(false)
|
||||
setError(e)
|
||||
}
|
||||
|
||||
const headerBg = useThemeColor('header')
|
||||
const numIconColor = useThemeColor('textDim')
|
||||
const textHint = useThemeColor('textDim')
|
||||
const inputBg = useThemeColor('background')
|
||||
|
||||
return (
|
||||
<Screen contentContainerStyle={$screen} preset="auto">
|
||||
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
||||
<Text preset="heading" text="Wallet recovery" style={{color: 'white', zIndex: 10}} />
|
||||
</View>
|
||||
|
||||
<View style={$contentContainer}>
|
||||
{mnemonicExists ? (
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text='Mnemonic exists'
|
||||
subText='Your wallet already has another mnemonic in its secure storage. Recovery process works only with freshly installed wallet to avoid loss of your funds.'
|
||||
leftIcon='faTriangleExclamation'
|
||||
// leftIconColor='red'
|
||||
style={$item}
|
||||
bottomSeparator={true}
|
||||
/>
|
||||
}
|
||||
FooterComponent={
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
onPress={onBack}
|
||||
text='Back'
|
||||
preset='secondary'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isValidMnemonic ? (
|
||||
<>
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text='Your mnemonic phrase'
|
||||
subText={mnemonic}
|
||||
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='1'/></View>}
|
||||
style={$item}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
text='Recovery from mints'
|
||||
subText='Identify mints to recover your ecash from and add them to the list.'
|
||||
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='2'/></View>}
|
||||
RightComponent={mintsStore.mintCount > 0 ? (
|
||||
<View style={$rightContainer}>
|
||||
<Button
|
||||
onPress={onAddMints}
|
||||
text='Mints'
|
||||
preset='secondary'
|
||||
/>
|
||||
</View>
|
||||
) : (undefined)
|
||||
}
|
||||
style={$item}
|
||||
/>
|
||||
{mintsStore.mintCount === 0 && (
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
onPress={onAddMints}
|
||||
text='Add mints'
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
ContentComponent={
|
||||
<>
|
||||
{mintsStore.mints.map((mint: Mint, index: number) => (
|
||||
<MintListItem
|
||||
key={mint.mintUrl}
|
||||
mint={mint}
|
||||
mintBalance={proofsStore.getMintBalance(mint.mintUrl)}
|
||||
// onMintSelect={() => onMintSelect(mint)}
|
||||
isSelectable={false}
|
||||
// isSelected={selectedMint?.mintUrl === mint.mintUrl}
|
||||
// isBlocked={mintsStore.isBlocked(mint.mintUrl as string)}
|
||||
separator={index === 0 ? 'both' : 'bottom'}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
FooterComponent={
|
||||
<>
|
||||
{mintsStore.mintCount > 0 && (
|
||||
<>
|
||||
<View style={$buttonContainer}>
|
||||
{startIndex > 0 && (
|
||||
<Button
|
||||
onPress={onComplete}
|
||||
text={'Complete'}
|
||||
style={{marginRight: spacing.small}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={startRecovery}
|
||||
text={startIndex === 0 ? 'Start recovery' : 'Next interval'}
|
||||
preset={startIndex === 0 ? 'default' : 'secondary'}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
text={`Recovery interval ${startIndex} - ${endIndex}`}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text='Insert backup mnemonic phrase'
|
||||
subText='Paste or rewrite 12 words phrase to recover your ecash balance on this device. Separate words by blank spaces.'
|
||||
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='1'/></View>}
|
||||
style={$item}
|
||||
/>
|
||||
}
|
||||
FooterComponent={
|
||||
<>
|
||||
<TextInput
|
||||
ref={mnemonicInputRef}
|
||||
onChangeText={(mnemonic: string) => setMnemonic(mnemonic)}
|
||||
value={mnemonic}
|
||||
numberOfLines={3}
|
||||
multiline={true}
|
||||
autoCapitalize='none'
|
||||
keyboardType='default'
|
||||
maxLength={150}
|
||||
placeholder='Mnemonic phrase...'
|
||||
selectTextOnFocus={true}
|
||||
style={[$mnemonicInput, {backgroundColor: inputBg, flexWrap: 'wrap'}]}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
{mnemonic ? (
|
||||
<Button
|
||||
onPress={onConfirm}
|
||||
text='Confirm'
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
onPress={onPaste}
|
||||
text='Paste'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<BottomModal
|
||||
isVisible={isErrorsModalVisible}
|
||||
style={{alignItems: 'stretch'}}
|
||||
ContentComponent={
|
||||
<>
|
||||
{recoveryErrors?.map((err, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
leftIcon='faTriangleExclamation'
|
||||
leftIconColor={colors.palette.angry500}
|
||||
text={err.message}
|
||||
subText={err.params ? err.params.mintUrl : ''}
|
||||
bottomSeparator={true}
|
||||
style={{paddingHorizontal: spacing.small}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleErrorsModal}
|
||||
onBackdropPress={toggleErrorsModal}
|
||||
/>
|
||||
<BottomModal
|
||||
isVisible={isResultModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
<>
|
||||
{resultModalInfo &&
|
||||
resultModalInfo.status === TransactionStatus.COMPLETED && (
|
||||
<>
|
||||
<ResultModalInfo
|
||||
icon="faCheckCircle"
|
||||
iconColor={colors.palette.success200}
|
||||
title="Recovery success!"
|
||||
message={resultModalInfo?.message}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.close'}
|
||||
onPress={toggleResultModal}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{resultModalInfo &&
|
||||
resultModalInfo.status === TransactionStatus.ERROR && (
|
||||
<>
|
||||
<ResultModalInfo
|
||||
icon="faTriangleExclamation"
|
||||
iconColor={colors.palette.angry500}
|
||||
title="Recovery failed"
|
||||
message={resultModalInfo?.message}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
text={'Show errors'}
|
||||
onPress={toggleErrorsModal}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{resultModalInfo &&
|
||||
resultModalInfo.status === TransactionStatus.EXPIRED && (
|
||||
<>
|
||||
<ResultModalInfo
|
||||
icon='faInfoCircle'
|
||||
iconColor={colors.palette.neutral400}
|
||||
title="No ecash recovered"
|
||||
message={resultModalInfo?.message}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.close'}
|
||||
onPress={toggleResultModal}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleResultModal}
|
||||
onBackdropPress={toggleResultModal}
|
||||
/>
|
||||
{error && <ErrorModal error={error} />}
|
||||
{info && <InfoModal message={info} />}
|
||||
{isLoading && <Loading statusMessage={statusMessage} style={{backgroundColor: headerBg, opacity: 1}}/>}
|
||||
</Screen>
|
||||
)
|
||||
})
|
||||
|
||||
const $screen: ViewStyle = {flex: 1}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
alignItems: 'center',
|
||||
padding: spacing.medium,
|
||||
height: spacing.screenHeight * 0.18,
|
||||
}
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
flex: 1,
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
padding: spacing.extraSmall,
|
||||
}
|
||||
|
||||
const $card: ViewStyle = {
|
||||
marginBottom: spacing.small,
|
||||
}
|
||||
|
||||
const $numIcon: ViewStyle = {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.medium
|
||||
}
|
||||
|
||||
const $mnemonicInput: TextStyle = {
|
||||
// flex: 1,
|
||||
borderRadius: spacing.small,
|
||||
fontSize: 16,
|
||||
padding: spacing.small,
|
||||
alignSelf: 'stretch',
|
||||
textAlignVertical: 'top',
|
||||
}
|
||||
|
||||
const $buttonContainer: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
marginTop: spacing.small,
|
||||
}
|
||||
|
||||
const $bottomModal: ViewStyle = {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.large,
|
||||
paddingHorizontal: spacing.small,
|
||||
}
|
||||
|
||||
const $item: ViewStyle = {
|
||||
paddingHorizontal: spacing.small,
|
||||
paddingLeft: 0,
|
||||
}
|
||||
|
||||
const $rightContainer: ViewStyle = {
|
||||
// padding: spacing.extraSmall,
|
||||
alignSelf: 'center',
|
||||
//marginLeft: spacing.small,
|
||||
}
|
||||
@@ -108,6 +108,7 @@ export const ScanScreen: FC<WalletStackScreenProps<'Scan'>> = function ScanScree
|
||||
return IncomingParser.navigateWithIncomingData(invoiceResult, navigation)
|
||||
|
||||
} catch (e: any) {
|
||||
log.trace('CATCH')
|
||||
const maybeLnurl = LnurlUtils.findEncodedLnurl(incoming)
|
||||
|
||||
if(maybeLnurl) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useState, useCallback} from 'react'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import React, {FC, useState, useCallback, useEffect} from 'react'
|
||||
import {Alert, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {spacing, useThemeColor, colors} from '../theme'
|
||||
import {WalletStackScreenProps} from '../navigation'
|
||||
import {
|
||||
@@ -20,10 +18,7 @@ import {
|
||||
import {useHeader} from '../utils/useHeader'
|
||||
import {log} from '../services/logService'
|
||||
import AppError from '../utils/AppError'
|
||||
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
||||
import { infoMessage } from '../utils/utils'
|
||||
import { IncomingDataType, IncomingParser } from '../services/incomingParser'
|
||||
import { LnurlUtils } from '../services/lnurl/lnurlUtils'
|
||||
import { IncomingDataType } from '../services/incomingParser'
|
||||
import { SvgXml } from 'react-native-svg'
|
||||
|
||||
export enum SendOption {
|
||||
@@ -41,11 +36,9 @@ export const SendOptionsScreen: FC<WalletStackScreenProps<'SendOptions'>> = obse
|
||||
leftIcon: 'faArrowLeft',
|
||||
onLeftPress: () => navigation.goBack(),
|
||||
})
|
||||
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
|
||||
|
||||
const gotoContacts = function () {
|
||||
navigation.navigate('ContactsNavigator', {
|
||||
screen: 'Contacts',
|
||||
|
||||
+76
-34
@@ -324,21 +324,26 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
|
||||
return
|
||||
}
|
||||
|
||||
setIsMintSelectorVisible(false)
|
||||
|
||||
if (paymentOption === SendOption.SHOW_TOKEN) {
|
||||
toggleQRModal()
|
||||
}
|
||||
|
||||
if (paymentOption === SendOption.SEND_TOKEN) {
|
||||
toggleNostrDMModal()
|
||||
}
|
||||
|
||||
setIsMintSelectorVisible(false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const onMintBalanceCancel = async function () {
|
||||
setIsMintSelectorVisible(false)
|
||||
|
||||
amountInputRef && amountInputRef.current
|
||||
? amountInputRef.current.focus()
|
||||
: false
|
||||
}
|
||||
|
||||
|
||||
@@ -638,7 +643,7 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
|
||||
gotoWallet={resetState}
|
||||
/>
|
||||
)}
|
||||
{(transactionStatus === TransactionStatus.PENDING || transactionStatus === TransactionStatus.COMPLETED) && (
|
||||
{(transactionStatus === TransactionStatus.COMPLETED) && (
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
@@ -676,7 +681,7 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
|
||||
encodedTokenToSend={encodedTokenToSend as string}
|
||||
onCopy={onCopy}
|
||||
onShareToApp={onShareToApp}
|
||||
handleError={handleError}
|
||||
// handleError={handleError}
|
||||
/>
|
||||
}
|
||||
onBackButtonPress={toggleQRModal}
|
||||
@@ -778,7 +783,7 @@ const MintBalanceSelector = observer(function (props: {
|
||||
const {mintsStore} = useStores()
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{flex: 1}}>
|
||||
<Card
|
||||
style={$card}
|
||||
heading={'Pay from'}
|
||||
@@ -806,20 +811,29 @@ const MintBalanceSelector = observer(function (props: {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
|
||||
<Button
|
||||
text="Send now"
|
||||
onPress={props.onMintBalanceConfirm}
|
||||
style={{marginRight: spacing.medium}}
|
||||
// LeftAccessory={() => <Icon icon="faCoins" color="white" size={spacing.medium} containerStyle={{marginRight: spacing.small}}/>}
|
||||
/>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.cancel'}
|
||||
onPress={props.onCancel}
|
||||
/>
|
||||
<View style={$bottomContainer}>
|
||||
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
|
||||
<Button
|
||||
text="Send now"
|
||||
onPress={props.onMintBalanceConfirm}
|
||||
style={{marginRight: spacing.medium}}
|
||||
LeftAccessory={() => (
|
||||
<Icon
|
||||
icon="faArrowUp"
|
||||
color="white"
|
||||
size={spacing.medium}
|
||||
/>
|
||||
)}
|
||||
// LeftAccessory={() => <Icon icon="faCoins" color="white" size={spacing.medium} containerStyle={{marginRight: spacing.small}}/>}
|
||||
/>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.cancel'}
|
||||
onPress={props.onCancel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -943,7 +957,7 @@ const SelectedMintBlock = observer(function (props: {
|
||||
const tokenTextColor = useThemeColor('textDim')
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={{flex: 1}}>
|
||||
<Card
|
||||
style={$card}
|
||||
heading={'Send from'}
|
||||
@@ -959,8 +973,9 @@ const SelectedMintBlock = observer(function (props: {
|
||||
separator={'top'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
/>
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
text='QR code'
|
||||
preset='secondary'
|
||||
@@ -978,7 +993,7 @@ const SelectedMintBlock = observer(function (props: {
|
||||
text='Send to contact'
|
||||
preset='secondary'
|
||||
onPress={props.toggleNostrDMModal}
|
||||
style={{marginLeft: spacing.medium}}
|
||||
style={{marginLeft: spacing.small}}
|
||||
LeftAccessory={() => (
|
||||
<Icon
|
||||
icon='faPaperPlane'
|
||||
@@ -988,7 +1003,15 @@ const SelectedMintBlock = observer(function (props: {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.close'}
|
||||
onPress={props.gotoWallet}
|
||||
style={{marginLeft: spacing.small}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
)
|
||||
})
|
||||
@@ -1001,18 +1024,37 @@ const SendAsQRCodeBlock = observer(function (props: {
|
||||
encodedTokenToSend: string
|
||||
onCopy: any
|
||||
onShareToApp: any
|
||||
handleError: any
|
||||
// handleError: any
|
||||
}) {
|
||||
|
||||
const [qrError, setQrError] = useState<Error | undefined>()
|
||||
|
||||
const handleQrError = function (error: Error) {
|
||||
setQrError(error)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[$bottomModal, {marginHorizontal: spacing.small}]}>
|
||||
<Text text={'Scan to receive'} />
|
||||
<View style={$qrCodeContainer}>
|
||||
<QRCode
|
||||
size={spacing.screenWidth - spacing.large * 2} value={props.encodedTokenToSend}
|
||||
onError={props.handleError}
|
||||
/>
|
||||
</View>
|
||||
<View style={[{marginHorizontal: spacing.small}]}>
|
||||
<Text text={'Scan to receive'} style={{alignSelf: 'center'}}/>
|
||||
|
||||
{qrError ? (
|
||||
<ListItem
|
||||
text='Could not display QR code, copy token instead.'
|
||||
subText={qrError ? qrError.message : ''}
|
||||
leftIcon='faTriangleExclamation'
|
||||
containerStyle={{marginVertical: spacing.large}}
|
||||
leftIconColor={colors.palette.angry500}
|
||||
/>
|
||||
) : (
|
||||
<View style={$qrCodeContainer}>
|
||||
<QRCode
|
||||
size={spacing.screenWidth - spacing.large * 2} value={props.encodedTokenToSend}
|
||||
onError={(error: any) => handleQrError(error)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
text="Share"
|
||||
@@ -1292,7 +1334,7 @@ const $profileIcon: ImageStyle = {
|
||||
}
|
||||
|
||||
const $bottomContainer: ViewStyle = {
|
||||
position: 'absolute',
|
||||
// position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
|
||||
@@ -1205,7 +1205,7 @@ const $contentContainer: TextStyle = {
|
||||
const $actionCard: ViewStyle = {
|
||||
marginBottom: spacing.extraSmall,
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
paddingTop: 0,
|
||||
paddingVertical: 0,
|
||||
}
|
||||
|
||||
const $dataCard: ViewStyle = {
|
||||
|
||||
@@ -70,7 +70,8 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
|
||||
const [amountToTransfer, setAmountToTransfer] = useState<string>('')
|
||||
const [invoiceExpiry, setInvoiceExpiry] = useState<Date | undefined>()
|
||||
const [paymentHash, setPaymentHash] = useState<string | undefined>()
|
||||
const [lnurlPayParams, setLnurlPayParams] = useState<LNURLPayParams | undefined>()
|
||||
const [lnurlPayParams, setLnurlPayParams] = useState<LNURLPayParams & {address?: string} | undefined>()
|
||||
const [isWaitingForFees, setIsWaitingForFees] = useState<boolean>(false)
|
||||
const [estimatedFee, setEstimatedFee] = useState<number>(0)
|
||||
const [finalFee, setFinalFee] = useState<number>(0)
|
||||
const [memo, setMemo] = useState('')
|
||||
@@ -158,6 +159,7 @@ useFocusEffect(
|
||||
|
||||
if(metadata) {
|
||||
let desc: string = ''
|
||||
let address: string = ''
|
||||
|
||||
for (const entry of metadata) {
|
||||
if (entry[0] === "text/plain") {
|
||||
@@ -166,9 +168,21 @@ useFocusEffect(
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of metadata) {
|
||||
if (entry[0] === "text/identifier" || entry[0] === "text/email") {
|
||||
address = entry[1];
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if(desc) {
|
||||
setLnurlDescription(desc)
|
||||
}
|
||||
|
||||
if(address) {
|
||||
// overwrite sender address set by wallet with the address from the lnurl response
|
||||
lnurlParams.address = address
|
||||
}
|
||||
}
|
||||
|
||||
const amountSats = roundUp(lnurlParams.minSendable / 1000, 0)
|
||||
@@ -302,29 +316,8 @@ const onAmountEndEditing = async function () {
|
||||
|
||||
setIsLoading(true)
|
||||
const encoded = await LnurlClient.getInvoice(lnurlPayParams, amount * 1000)
|
||||
|
||||
const invoice = LightningUtils.decodeInvoice(encoded)
|
||||
const {description_hash} = LightningUtils.getInvoiceData(invoice)
|
||||
|
||||
if(!description_hash || description_hash.length === 0) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Invoice from ${lnurlPayParams.domain} is invalid, missing description_hash`)
|
||||
}
|
||||
// check that retrieved invoice matches the previous LNURL pay link
|
||||
const hashedMetadata = QuickCrypto.createHash('sha256')
|
||||
.update(lnurlPayParams.metadata)
|
||||
.digest('hex')
|
||||
|
||||
log.trace('hashedMetadata', hashedMetadata)
|
||||
log.trace('description_hash', description_hash)
|
||||
|
||||
if(hashedMetadata !== description_hash) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
`Invoice from ${lnurlPayParams.domain} has invalid description_hash ${description_hash}, expected ${hashedMetadata}`
|
||||
)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
if(encoded) {
|
||||
return onEncodedInvoice(encoded)
|
||||
}
|
||||
@@ -347,7 +340,7 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
|
||||
setEncodedInvoice(encoded)
|
||||
|
||||
const invoice = LightningUtils.decodeInvoice(encoded)
|
||||
const {amount, expiry, description, description_hash, timestamp} = LightningUtils.getInvoiceData(invoice)
|
||||
const {amount, expiry, description, timestamp} = LightningUtils.getInvoiceData(invoice)
|
||||
|
||||
// log.trace('Decoded invoice', invoice, 'onEncodedInvoice')
|
||||
log.trace('Invoice data', {amount, expiry, description}, 'onEncodedInvoice')
|
||||
@@ -522,7 +515,7 @@ const satsColor = colors.palette.primary200
|
||||
style={[$card, {minHeight: 0}]}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text={(memo) ? memo : lnurlPayParams ? lnurlPayParams.address ? lnurlPayParams.address : lnurlPayParams.domain : ''}
|
||||
text={lnurlPayParams?.address || memo || lnurlPayParams?.domain || 'Payment info'}
|
||||
subText={lnurlDescription}
|
||||
LeftComponent={
|
||||
<Icon
|
||||
|
||||
@@ -49,7 +49,8 @@ import AppError, { Err } from '../utils/AppError'
|
||||
import {
|
||||
APP_ENV,
|
||||
CODEPUSH_STAGING_DEPLOYMENT_KEY,
|
||||
CODEPUSH_PRODUCTION_DEPLOYMENT_KEY,
|
||||
CODEPUSH_PRODUCTION_DEPLOYMENT_KEY,
|
||||
MINIBITS_MINT_URL
|
||||
} from '@env'
|
||||
import { round } from '../utils/number'
|
||||
import { NotificationService } from '../services/notificationService'
|
||||
@@ -89,7 +90,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
|
||||
const [info, setInfo] = useState<string>('')
|
||||
const [defaultMintUrl, setDefaultMintUrl] = useState<string>('https://mint.minibits.cash/Bitcoin')
|
||||
const [defaultMintUrl, setDefaultMintUrl] = useState<string>(MINIBITS_MINT_URL)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
|
||||
@@ -107,18 +108,24 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
setUpdateDescription(update.description)
|
||||
setUpdateSize(`${round(update.packageSize * 0.000001, 2)}MB`)
|
||||
setIsUpdateAvailable(true)
|
||||
toggleUpdateModal()
|
||||
// toggleUpdateModal()
|
||||
log.info('OTA Update available', update, 'checkForUpdate')
|
||||
}
|
||||
} catch (e: any) {
|
||||
return false // silent
|
||||
}
|
||||
}
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
|
||||
setTimeout(() => {
|
||||
if(!isInternetReachable) {
|
||||
return
|
||||
}
|
||||
checkForUpdate()
|
||||
})
|
||||
}, 100)
|
||||
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
const handleBinaryVersionMismatchCallback = function(update: RemotePackage) {
|
||||
log.info('Native update available, silent', update)
|
||||
@@ -147,13 +154,13 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
}
|
||||
|
||||
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
setTimeout(async () => {
|
||||
if(!isInternetReachable) {
|
||||
return
|
||||
}
|
||||
// subscribe once to receive tokens or payment requests by NOSTR DMs
|
||||
Wallet.checkPendingReceived()
|
||||
})
|
||||
}, 200)
|
||||
|
||||
EventEmitter.on('receiveTokenCompleted', onReceiveTokenCompleted)
|
||||
EventEmitter.on('receivePaymentRequest', onReceivePaymentRequest)
|
||||
@@ -201,14 +208,14 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
setTimeout(() => {
|
||||
if(!isInternetReachable) {
|
||||
return
|
||||
}
|
||||
|
||||
Wallet.checkPendingSpent()
|
||||
Wallet.checkPendingTopups()
|
||||
})
|
||||
}, 100)
|
||||
}, [])
|
||||
)
|
||||
|
||||
@@ -232,10 +239,14 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
appState.current.match(/inactive|background/) &&
|
||||
nextAppState === 'active') {
|
||||
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
setTimeout(() => {
|
||||
if(!isInternetReachable) {
|
||||
return
|
||||
}
|
||||
|
||||
Wallet.checkPendingSpent()
|
||||
Wallet.checkPendingTopups()
|
||||
})
|
||||
Wallet.checkPendingTopups()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
appState.current = nextAppState
|
||||
|
||||
@@ -15,16 +15,20 @@ import {
|
||||
// import { isRTL } from "../i18n"
|
||||
import {useStores} from '../models'
|
||||
import {AppStackScreenProps} from '../navigation'
|
||||
import {spacing, colors} from '../theme'
|
||||
import {spacing, colors, useThemeColor} from '../theme'
|
||||
import {useHeader} from '../utils/useHeader'
|
||||
import {useSafeAreaInsetsStyle} from '../utils/useSafeAreaInsetsStyle'
|
||||
import {
|
||||
Button,
|
||||
ErrorModal,
|
||||
Icon,
|
||||
Loading,
|
||||
Screen,
|
||||
Text,
|
||||
} from '../components'
|
||||
import {TxKeyPath} from '../i18n'
|
||||
import AppError from '../utils/AppError'
|
||||
import { MintClient } from '../services'
|
||||
|
||||
|
||||
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
||||
@@ -77,14 +81,35 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
|
||||
})
|
||||
|
||||
const {userSettingsStore} = useStores()
|
||||
|
||||
const [isGotoWalletVisible, setIsGotoWalletVisible] = useState<boolean>(false)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
|
||||
|
||||
|
||||
const gotoWallet = function () {
|
||||
userSettingsStore.setIsOnboarded(true)
|
||||
navigation.navigate('Tabs')
|
||||
const gotoWallet = async function () {
|
||||
try {
|
||||
// do not overwrite if one was set during recovery
|
||||
setIsLoading(true)
|
||||
const mnemonic = await MintClient.getOrCreateMnemonic()
|
||||
userSettingsStore.setIsOnboarded(true)
|
||||
navigation.navigate('Tabs')
|
||||
setIsLoading(false)
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const gotoRecovery = async function () {
|
||||
try {
|
||||
navigation.navigate('RemoteRecovery')
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = function (e: AppError) {
|
||||
setIsLoading(false)
|
||||
setError(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -121,15 +146,7 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
|
||||
[]
|
||||
)
|
||||
|
||||
const onPageSelected = function(e: any) {
|
||||
if(e.nativeEvent.position === PAGES.length - 1) {
|
||||
setIsGotoWalletVisible(true)
|
||||
} else {
|
||||
setIsGotoWalletVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
const $bottomContainerInsets = useSafeAreaInsetsStyle(['bottom'])
|
||||
const headerBg = useThemeColor('header')
|
||||
|
||||
const renderBullet = ({item}: {item: {id: string; tx: string}}) => (
|
||||
<View style={$listItem}>
|
||||
@@ -150,7 +167,7 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
|
||||
|
||||
|
||||
return (
|
||||
<Screen contentContainerStyle={$container} preset="fixed">
|
||||
<Screen contentContainerStyle={$container} preset="fixed" style={{backgroundColor: headerBg}}>
|
||||
<AnimatedPagerView
|
||||
testID="pager-view"
|
||||
initialPage={0}
|
||||
@@ -192,6 +209,13 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
|
||||
preset='secondary'
|
||||
text='Got it, take me to the wallet'
|
||||
/>
|
||||
<Button
|
||||
onPress={gotoRecovery}
|
||||
preset='tertiary'
|
||||
text='Recover lost wallet'
|
||||
LeftAccessory={() => {return<Icon icon='faRotate'/>}}
|
||||
style={{marginTop: spacing.medium}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -212,7 +236,8 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{error && <ErrorModal error={error} />}
|
||||
{isLoading && <Loading statusMessage={'Creating wallet seed, this takes a while...'} style={{backgroundColor: headerBg, opacity: 1}}/>}
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
@@ -230,12 +255,11 @@ const $dotContainer: ViewStyle ={
|
||||
const $container: ViewStyle = {
|
||||
// alignItems: 'center',
|
||||
flex: 1,
|
||||
padding: spacing.medium,
|
||||
backgroundColor: colors.palette.primary500
|
||||
padding: spacing.medium,
|
||||
}
|
||||
|
||||
const $listContainer: ViewStyle = {
|
||||
maxHeight: spacing.screenHeight * 0.35,
|
||||
maxHeight: spacing.screenHeight * 0.4,
|
||||
}
|
||||
|
||||
const $listItem: ViewStyle = {
|
||||
@@ -249,8 +273,7 @@ const $itemIcon: ViewStyle = {
|
||||
marginBottom: spacing.small,
|
||||
}
|
||||
|
||||
const $buttonContainer: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
const $buttonContainer: ViewStyle = {
|
||||
alignSelf: 'center',
|
||||
marginTop: spacing.large,
|
||||
}
|
||||
@@ -267,6 +290,6 @@ const $welcomeIntro: TextStyle = {
|
||||
}
|
||||
|
||||
const $welcomeFinal: TextStyle = {
|
||||
marginTop: spacing.huge,
|
||||
marginTop: spacing.large,
|
||||
color: 'white',
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ export * from './SecurityScreen'
|
||||
export * from './PrivacyScreen'
|
||||
export * from './UpdateScreen'
|
||||
export * from './BackupScreen'
|
||||
export * from './RemoteBackupScreen'
|
||||
export * from './RemoteRecoveryScreen'
|
||||
export * from './LocalRecoveryScreen'
|
||||
export * from './DeveloperScreen'
|
||||
export * from './RelaysScreen'
|
||||
|
||||
+208
-39
@@ -5,26 +5,86 @@ import {
|
||||
PayLnInvoiceResponse,
|
||||
type Proof as CashuProof,
|
||||
} from '@cashu/cashu-ts'
|
||||
import { TorDaemon } from '../services'
|
||||
import {rootStoreInstance} from '../models'
|
||||
import { KeyChain, TorDaemon } from '../services'
|
||||
import {CashuUtils} from './cashu/cashuUtils'
|
||||
import AppError, {Err} from '../utils/AppError'
|
||||
import {log} from './logService'
|
||||
import {Token} from '../models/Token'
|
||||
import {Proof} from '../models/Proof'
|
||||
import { deriveSeedFromMnemonic } from '@cashu/cashu-ts'
|
||||
|
||||
export type MintKeys = {[k: number]: string}
|
||||
export type MintKeySets = {keysets: Array<string>}
|
||||
|
||||
let _mints: {[mintUrl: string]: CashuMint} = {}
|
||||
let _wallets: {[mintUrl: string]: CashuWallet} = {}
|
||||
let _wallets: {[mintUrl: string]: CashuWallet} = {} // used where seed is not required (perf)
|
||||
let _seedWallets: {[mintUrl: string]: CashuWallet} = {} // wallet instances with seeds from keystore
|
||||
let _mnemonic: string | undefined = undefined
|
||||
let _seed: Uint8Array | undefined = undefined
|
||||
|
||||
const getMint = function (mintUrl: string) {
|
||||
const { mintsStore } = rootStoreInstance
|
||||
|
||||
const getOrCreateMnemonic = async function (): Promise<string> {
|
||||
let mnemonic: string | undefined = undefined
|
||||
|
||||
mnemonic = await getMnemonic() // returns cached or saved mnemonic
|
||||
|
||||
if (!mnemonic) {
|
||||
mnemonic = KeyChain.generateMnemonic() as string
|
||||
const seed = deriveSeedFromMnemonic(mnemonic) // expensive
|
||||
|
||||
await KeyChain.saveMnemonic(mnemonic)
|
||||
await KeyChain.saveSeed(seed)
|
||||
|
||||
log.trace('[getOrCreateMnemonic]', 'Created and saved new mnemonic and seed')
|
||||
}
|
||||
|
||||
return mnemonic
|
||||
}
|
||||
|
||||
// caching mnemonic
|
||||
const getMnemonic = async function (): Promise<string | undefined> {
|
||||
if (_mnemonic) {
|
||||
log.trace('[getMnemonic]', 'returning cached mnemonic')
|
||||
return _mnemonic
|
||||
}
|
||||
|
||||
const mnemonic = await KeyChain.loadMnemonic() as string
|
||||
|
||||
if (!mnemonic) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
_mnemonic = mnemonic
|
||||
return mnemonic
|
||||
}
|
||||
|
||||
|
||||
const getSeed = async function (): Promise<Uint8Array | undefined> {
|
||||
if (_seed) {
|
||||
log.trace('[getSeed]', 'returning cached seed')
|
||||
return _seed
|
||||
}
|
||||
|
||||
const seed = await KeyChain.loadSeed()
|
||||
|
||||
if (!seed) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
_seed = seed
|
||||
return seed
|
||||
}
|
||||
|
||||
|
||||
const getMint = function (mintUrl: string) {
|
||||
if (_mints[mintUrl]) {
|
||||
return _mints[mintUrl]
|
||||
}
|
||||
|
||||
if(mintUrl.includes('.onion')) {
|
||||
log.trace('Creating mint instance with .onion mintUrl', {mintUrl}, 'getMint')
|
||||
log.trace('[getMint]', 'Creating mint instance with .onion mintUrl', {mintUrl})
|
||||
const mint = new CashuMint(mintUrl, TorDaemon.torRequest)
|
||||
_mints[mintUrl] = mint
|
||||
|
||||
@@ -37,16 +97,55 @@ const getMint = function (mintUrl: string) {
|
||||
return mint
|
||||
}
|
||||
|
||||
const getWallet = function (mintUrl: string) {
|
||||
if (_wallets[mintUrl]) {
|
||||
return _wallets[mintUrl]
|
||||
}
|
||||
|
||||
const mint = getMint(mintUrl)
|
||||
const wallet = new CashuWallet(mint)
|
||||
_wallets[mintUrl] = wallet
|
||||
const getWallet = async function (
|
||||
mintUrl: string,
|
||||
withSeed: boolean = false
|
||||
) {
|
||||
log.trace('[getWallet] start')
|
||||
|
||||
return wallet
|
||||
if (withSeed && _seedWallets[mintUrl]) {
|
||||
log.trace('[getWallet]', 'Returning existing cashuWallet instance with seed')
|
||||
return _seedWallets[mintUrl]
|
||||
}
|
||||
|
||||
if (!withSeed && _wallets[mintUrl]) {
|
||||
log.trace('[getWallet]', 'Returning existing cashuWallet instance')
|
||||
return _wallets[mintUrl]
|
||||
}
|
||||
|
||||
const cashuMint = getMint(mintUrl)
|
||||
const mint = mintsStore.findByUrl(mintUrl)
|
||||
|
||||
if(withSeed) {
|
||||
let seed: Uint8Array | undefined = undefined
|
||||
seed = await getSeed()
|
||||
|
||||
// Handle legacy pre-0.1.5 created wallets
|
||||
if(!seed) {
|
||||
const mnemonic = await getOrCreateMnemonic()
|
||||
seed = await getSeed()
|
||||
resetCachedWallets() // force all wallet instances to be recreated with seed
|
||||
}
|
||||
|
||||
const seedWallet = new CashuWallet(cashuMint, mint ? mint.keys : undefined, seed)
|
||||
log.trace('[getWallet]', 'Returning new cashuWallet instance with seed')
|
||||
|
||||
_seedWallets[mintUrl] = seedWallet
|
||||
return seedWallet
|
||||
}
|
||||
|
||||
const wallet = new CashuWallet(cashuMint, mint ? mint.keys : undefined)
|
||||
_wallets[mintUrl] = wallet
|
||||
|
||||
log.trace('[getWallet]', 'Returning new cashuWallet instance')
|
||||
return wallet
|
||||
}
|
||||
|
||||
|
||||
const resetCachedWallets = function () {
|
||||
_seedWallets = {}
|
||||
_wallets = {}
|
||||
}
|
||||
|
||||
const getMintKeys = async function (mintUrl: string) {
|
||||
@@ -56,13 +155,12 @@ const getMintKeys = async function (mintUrl: string) {
|
||||
|
||||
try {
|
||||
// keysets = await mint.getKeySets()
|
||||
log.trace('Sending getKeys request', {mintUrl}, 'getMintKeys')
|
||||
keys = await mint.getKeys()
|
||||
} catch (e: any) {
|
||||
throw new AppError(
|
||||
Err.CONNECTION_ERROR,
|
||||
`Could not connect to the selected mint.`,
|
||||
[e.message, mintUrl],
|
||||
{message: e.message, mintUrl},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +168,7 @@ const getMintKeys = async function (mintUrl: string) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Invalid keys retrieved from the selected mint.',
|
||||
[mintUrl, keys],
|
||||
{mintUrl, keys},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,47 +183,61 @@ const getMintKeys = async function (mintUrl: string) {
|
||||
}
|
||||
|
||||
|
||||
const receiveFromMint = async function (mintUrl: string, encodedToken: string) {
|
||||
|
||||
const receiveFromMint = async function (
|
||||
mintUrl: string,
|
||||
encodedToken: string,
|
||||
counter: number
|
||||
) {
|
||||
try {
|
||||
const cashuWallet = getWallet(mintUrl)
|
||||
const cashuWallet = await getWallet(mintUrl, true) // with seed
|
||||
|
||||
log.trace('[receiveFromMint] calling cashuWallet.receive', {encodedToken, counter})
|
||||
|
||||
// this method returns quite a mess, we normalize naming of returned parameters
|
||||
const {token, tokensWithErrors, newKeys} = await cashuWallet.receive(
|
||||
const {token, tokensWithErrors, newKeys, errors} = await cashuWallet.receive(
|
||||
encodedToken,
|
||||
undefined,
|
||||
counter
|
||||
)
|
||||
|
||||
log.trace('[receiveFromMint] updatedToken', token)
|
||||
log.trace('[receiveFromMint] tokensWithErrors', tokensWithErrors)
|
||||
log.trace('[receiveFromMint] newKeys', newKeys)
|
||||
|
||||
// if (newKeys) { _setKeys(mintUrl, newKeys) }
|
||||
log.trace('[receiveFromMint] errors', errors)
|
||||
|
||||
return {
|
||||
updatedToken: token as Token,
|
||||
errorToken: tokensWithErrors as Token,
|
||||
newKeys,
|
||||
errors
|
||||
}
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.MINT_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const sendFromMint = async function (
|
||||
mintUrl: string,
|
||||
amountToSend: number,
|
||||
proofsToSendFrom: Proof[],
|
||||
counter: number
|
||||
) {
|
||||
try {
|
||||
const cashuWallet = getWallet(mintUrl)
|
||||
const cashuWallet = await getWallet(mintUrl, true) // with seed
|
||||
|
||||
const {returnChange, send, newKeys} = await cashuWallet.send(
|
||||
amountToSend,
|
||||
proofsToSendFrom,
|
||||
undefined,
|
||||
counter
|
||||
)
|
||||
|
||||
log.trace('[MintClient.sendFromMint] returnedProofs', returnChange)
|
||||
log.trace('[MintClient.sendFromMint] sentProofs', send)
|
||||
log.trace('[MintClient.sendFromMint] newKeys', newKeys)
|
||||
log.debug('[MintClient.sendFromMint] returnedProofs', returnChange)
|
||||
log.debug('[MintClient.sendFromMint] sentProofs', send)
|
||||
log.debug('[MintClient.sendFromMint] newKeys', newKeys)
|
||||
|
||||
// do some basic validations that proof amounts from mints match
|
||||
const totalAmountToSendFrom = CashuUtils.getProofsAmount(proofsToSendFrom)
|
||||
@@ -157,7 +269,7 @@ const sendFromMint = async function (
|
||||
} catch (e: any) {
|
||||
throw new AppError(
|
||||
Err.MINT_ERROR,
|
||||
`The mint could not return proofs necessary for this transaction. ${e.message}`,
|
||||
`The mint could not return signatures necessary for this transaction. ${e.message}`,
|
||||
{
|
||||
caller: 'sendFromMint',
|
||||
mintUrl,
|
||||
@@ -167,12 +279,14 @@ const sendFromMint = async function (
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const getSpentOrPendingProofsFromMint = async function (
|
||||
mintUrl: string,
|
||||
proofs: Proof[],
|
||||
) {
|
||||
try {
|
||||
const cashuWallet = getWallet(mintUrl)
|
||||
const cashuWallet = await getWallet(mintUrl)
|
||||
|
||||
const spentPendingProofs = await cashuWallet.checkProofsSpent(proofs)
|
||||
|
||||
@@ -186,7 +300,7 @@ const getSpentOrPendingProofsFromMint = async function (
|
||||
} catch (e: any) {
|
||||
throw new AppError(
|
||||
Err.MINT_ERROR,
|
||||
'The mint could not check if the proofs were spent or are pending.',
|
||||
'The mint could not reply if the proofs are spent or pending.',
|
||||
{
|
||||
caller: 'getSpentOrPendingProofsFromMint',
|
||||
mintUrl,
|
||||
@@ -196,6 +310,8 @@ const getSpentOrPendingProofsFromMint = async function (
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const getLightningFee = async function (
|
||||
mintUrl: string,
|
||||
encodedInvoice: string,
|
||||
@@ -218,20 +334,24 @@ const getLightningFee = async function (
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const payLightningInvoice = async function (
|
||||
mintUrl: string,
|
||||
encodedInvoice: string,
|
||||
proofsToPayFrom: CashuProof[],
|
||||
estimatedFee: number,
|
||||
counter: number
|
||||
) {
|
||||
try {
|
||||
const cashuWallet = getWallet(mintUrl)
|
||||
const cashuWallet = await getWallet(mintUrl, true) // with seed
|
||||
|
||||
const {isPaid, change, preimage, newKeys}: PayLnInvoiceResponse =
|
||||
await cashuWallet.payLnInvoice(
|
||||
encodedInvoice,
|
||||
proofsToPayFrom,
|
||||
estimatedFee,
|
||||
counter
|
||||
)
|
||||
|
||||
// if (newKeys) { _setKeys(mintUrl, newKeys) }
|
||||
@@ -261,12 +381,14 @@ const payLightningInvoice = async function (
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const requestLightningInvoice = async function (
|
||||
mintUrl: string,
|
||||
amount: number,
|
||||
) {
|
||||
try {
|
||||
const cashuWallet = getWallet(mintUrl)
|
||||
const cashuWallet = await getWallet(mintUrl)
|
||||
const {pr, hash} = await cashuWallet.requestMint(amount)
|
||||
|
||||
log.info('[requestLightningInvoice]', {pr, hash})
|
||||
@@ -280,17 +402,22 @@ const requestLightningInvoice = async function (
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const requestProofs = async function (
|
||||
mintUrl: string,
|
||||
amount: number,
|
||||
paymentHash: string,
|
||||
counter: number
|
||||
) {
|
||||
try {
|
||||
const cashuWallet = getWallet(mintUrl)
|
||||
const cashuWallet = await getWallet(mintUrl, true) // with seed
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const {proofs, newKeys} = await cashuWallet.requestTokens(
|
||||
amount,
|
||||
paymentHash,
|
||||
undefined,
|
||||
counter
|
||||
)
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -312,13 +439,55 @@ const requestProofs = async function (
|
||||
}
|
||||
}
|
||||
|
||||
export const MintClient = {
|
||||
getMintKeys,
|
||||
receiveFromMint,
|
||||
sendFromMint,
|
||||
getSpentOrPendingProofsFromMint,
|
||||
getLightningFee,
|
||||
payLightningInvoice,
|
||||
requestLightningInvoice,
|
||||
requestProofs,
|
||||
const restore = async function (
|
||||
mintUrl: string,
|
||||
indexFrom: number,
|
||||
indexTo: number,
|
||||
seed: Uint8Array
|
||||
) {
|
||||
try {
|
||||
// need special wallet instance to pass seed directly
|
||||
const cashuMint = getMint(mintUrl)
|
||||
const mint = mintsStore.findByUrl(mintUrl)
|
||||
|
||||
if(!mint) {
|
||||
throw new AppError(Err.MINT_ERROR, 'Could not find mint in wallet state', {caller: 'restore', mintUrl})
|
||||
}
|
||||
|
||||
const seedWallet = new CashuWallet(cashuMint, mint.keys, seed)
|
||||
const count = Math.abs(indexTo - indexFrom)
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const {proofs, newKeys} = await seedWallet.restore(
|
||||
count,
|
||||
indexFrom,
|
||||
)
|
||||
/* eslint-enable */
|
||||
|
||||
log.info('[restore]', 'Number of recovered proofs', {proofs: proofs.length, newKeys})
|
||||
|
||||
return {
|
||||
proofs: proofs || [],
|
||||
newKeys
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.error(e)
|
||||
throw new AppError(Err.MINT_ERROR, e.message, {mintUrl})
|
||||
}
|
||||
}
|
||||
|
||||
export const MintClient = {
|
||||
getOrCreateMnemonic,
|
||||
getMnemonic,
|
||||
getSeed,
|
||||
resetCachedWallets,
|
||||
getMintKeys,
|
||||
receiveFromMint,
|
||||
sendFromMint,
|
||||
getSpentOrPendingProofsFromMint,
|
||||
getLightningFee,
|
||||
payLightningInvoice,
|
||||
requestLightningInvoice,
|
||||
requestProofs,
|
||||
restore,
|
||||
}
|
||||
|
||||
@@ -163,8 +163,7 @@ const navigateWithIncomingData = async function (
|
||||
|
||||
if(lnurlParams.tag === 'payRequest') {
|
||||
return navigation.navigate('Transfer', {
|
||||
lnurlParams,
|
||||
// encodedInvoice,
|
||||
lnurlParams,
|
||||
paymentOption: SendOption.LNURL_PAY
|
||||
})
|
||||
}
|
||||
@@ -174,8 +173,7 @@ const navigateWithIncomingData = async function (
|
||||
const addressParamsResult = await LnurlClient.getLnurlAddressParams(incoming.encoded)
|
||||
|
||||
return navigation.navigate('Transfer', {
|
||||
lnurlParams: addressParamsResult.lnurlParams,
|
||||
// encodedInvoice: addressParamsResult.encodedInvoice,
|
||||
lnurlParams: addressParamsResult.lnurlParams,
|
||||
paymentOption: SendOption.LNURL_PAY
|
||||
})
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./minibitsService"
|
||||
export * from "./nostrService"
|
||||
export * from "./logService"
|
||||
export * from "./torService"
|
||||
// export * from "./restoreService"
|
||||
|
||||
+282
-121
@@ -1,13 +1,17 @@
|
||||
import * as _Keychain from 'react-native-keychain'
|
||||
import AppError, {Err} from '../utils/AppError'
|
||||
import QuickCrypto from 'react-native-quick-crypto'
|
||||
import { generateNewMnemonic } from '@cashu/cashu-ts'
|
||||
import * as nostrTools from 'nostr-tools'
|
||||
import {btoa, atob, fromByteArray} from 'react-native-quick-base64'
|
||||
import {log} from './logService'
|
||||
|
||||
|
||||
export enum KeyChainServiceName {
|
||||
MMKV = 'app.minibits.mmkv',
|
||||
NOSTR = 'app.minibits.nostr',
|
||||
SEED = 'app.minibits.seed',
|
||||
MNEMONIC = 'app.minibits.mnemonic',
|
||||
}
|
||||
|
||||
export type KeyPair = {
|
||||
@@ -15,15 +19,170 @@ export type KeyPair = {
|
||||
privateKey: string
|
||||
}
|
||||
|
||||
/* const generateKeyPair = async function () {
|
||||
try {
|
||||
const keyPair = QuickCrypto.generateKeyPairSync('ed25519')
|
||||
log.trace('New ed25519 keyPair:', keyPair)
|
||||
return keyPair
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
|
||||
|
||||
const getSupportedBiometryType = async function () {
|
||||
try {
|
||||
const biometryType = await _Keychain.getSupportedBiometryType({})
|
||||
log.trace('biometryType', biometryType)
|
||||
return biometryType
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const generateMnemonic = function () {
|
||||
try {
|
||||
log.trace('[generateMnemonic]', 'start')
|
||||
|
||||
const mnemonic = generateNewMnemonic()
|
||||
|
||||
log.trace('[generateMnemonic]', 'New mnemonic created:', mnemonic)
|
||||
|
||||
return mnemonic as string
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const saveMnemonic = async function (
|
||||
mnemonic: string,
|
||||
): Promise<_Keychain.Result | false> {
|
||||
try {
|
||||
const result = await _Keychain.setGenericPassword(
|
||||
KeyChainServiceName.MNEMONIC,
|
||||
mnemonic,
|
||||
{
|
||||
service: KeyChainServiceName.MNEMONIC
|
||||
},
|
||||
)
|
||||
|
||||
log.trace('[saveMnemonic]', 'Saved new mnemonic to the KeyChain', mnemonic)
|
||||
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
* Loads mnemonic from the KeyChain/KeyStore
|
||||
*
|
||||
*/
|
||||
const loadMnemonic = async function (): Promise<string | undefined> {
|
||||
try {
|
||||
log.trace('[loadMnemonic]', 'start')
|
||||
|
||||
const result = await _Keychain.getGenericPassword({
|
||||
service: KeyChainServiceName.MNEMONIC
|
||||
})
|
||||
|
||||
|
||||
if (result) {
|
||||
const mnemonic = result.password
|
||||
log.trace('[loadMnemonic]', 'complete', {mnemonic})
|
||||
return mnemonic
|
||||
}
|
||||
|
||||
log.trace('[loadMnemonic]', 'Did not find existing mnemonic in the KeyChain')
|
||||
return undefined
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes mnemonic from KeyChain/KeyStore
|
||||
*
|
||||
* @param service The key to kill.
|
||||
*/
|
||||
const removeMnemonic = async function (): Promise<boolean> {
|
||||
try {
|
||||
const result = await _Keychain.resetGenericPassword({
|
||||
service: KeyChainServiceName.MNEMONIC
|
||||
})
|
||||
log.trace('[removeMnemonic]', 'Removed mnemonic.')
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save seed to KeyChain/KeyStore
|
||||
*
|
||||
* @param seed The key to save.
|
||||
*/
|
||||
const saveSeed = async function (
|
||||
seed: Uint8Array,
|
||||
): Promise<_Keychain.Result | false> {
|
||||
try {
|
||||
|
||||
const seedStr: string = Buffer.from(seed).toString('base64')
|
||||
|
||||
log.trace('[saveSeed]', 'Saved seed base64', seedStr)
|
||||
|
||||
const result = await _Keychain.setGenericPassword(
|
||||
KeyChainServiceName.SEED,
|
||||
seedStr,
|
||||
{
|
||||
service: KeyChainServiceName.SEED
|
||||
},
|
||||
)
|
||||
|
||||
log.trace('[saveSeed]', 'Saved seed to the KeyChain')
|
||||
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads mnemonic from the KeyChain/KeyStore
|
||||
*
|
||||
*/
|
||||
const loadSeed = async function (): Promise<Uint8Array | undefined> {
|
||||
try {
|
||||
log.trace('[loadSeed]', 'start')
|
||||
const result = await _Keychain.getGenericPassword({
|
||||
service: KeyChainServiceName.SEED
|
||||
})
|
||||
|
||||
if (result) {
|
||||
const seedStr = result.password
|
||||
log.trace('[loadSeed]', 'base64', {seedStr})
|
||||
const seed: Uint8Array = new Uint8Array(Buffer.from(seedStr, 'base64'))
|
||||
return seed
|
||||
}
|
||||
|
||||
log.trace('[loadSeed]', 'Did not find existing seed in the KeyChain')
|
||||
return undefined
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes mnemonic from KeyChain/KeyStore
|
||||
*
|
||||
* @param service The key to kill.
|
||||
*/
|
||||
const removeSeed = async function (): Promise<boolean> {
|
||||
try {
|
||||
const result = await _Keychain.resetGenericPassword({
|
||||
service: KeyChainServiceName.SEED
|
||||
})
|
||||
log.trace('[removeSeed]', 'Removed seed.')
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const generateNostrKeyPair = function () {
|
||||
@@ -39,33 +198,6 @@ const generateNostrKeyPair = function () {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const generateMmkvEncryptionKey = (): string => {
|
||||
try {
|
||||
const keyLength = 32 // Length of the encryption key in bytes
|
||||
const encryptionKey = QuickCrypto.randomBytes(keyLength)
|
||||
const uint8Array = new Uint8Array(encryptionKey)
|
||||
const stringKey = fromByteArray(uint8Array)
|
||||
const base64Key = btoa(stringKey)
|
||||
|
||||
log.trace('New Base64 encryptionKey created:', base64Key)
|
||||
|
||||
return base64Key
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message, [e])
|
||||
}
|
||||
}
|
||||
|
||||
const getSupportedBiometryType = async function () {
|
||||
try {
|
||||
const biometryType = await _Keychain.getSupportedBiometryType({})
|
||||
log.trace('biometryType', biometryType)
|
||||
return biometryType
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save keypair to KeyChain/KeyStore
|
||||
*
|
||||
@@ -83,7 +215,7 @@ const saveNostrKeyPair = async function (
|
||||
},
|
||||
)
|
||||
|
||||
log.trace('Saved keypair to the KeyChain', '', 'saveNostrKeyPair')
|
||||
log.trace('[saveNostrKeyPair]', 'Saved keypair to the KeyChain')
|
||||
|
||||
return result
|
||||
} catch (e: any) {
|
||||
@@ -96,51 +228,59 @@ const saveNostrKeyPair = async function (
|
||||
*
|
||||
*/
|
||||
const loadNostrKeyPair = async function (): Promise<KeyPair | undefined> {
|
||||
try {
|
||||
const result = await _Keychain.getGenericPassword({
|
||||
service: KeyChainServiceName.NOSTR
|
||||
})
|
||||
try {
|
||||
const result = await _Keychain.getGenericPassword({
|
||||
service: KeyChainServiceName.NOSTR
|
||||
})
|
||||
|
||||
if (result) {
|
||||
const keyPair = JSON.parse(result.password)
|
||||
return keyPair
|
||||
if (result) {
|
||||
const keyPair = JSON.parse(result.password)
|
||||
return keyPair
|
||||
}
|
||||
log.trace('[loadNostrKeyPair]', 'Did not find existing keyPair in the KeyChain')
|
||||
return undefined
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
log.trace('Did not find existing keyPair in the KeyChain', '', 'loadNostrKeyPair')
|
||||
return undefined
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads keypair from KeyChain/KeyStore
|
||||
*
|
||||
* Removes keypair from KeyChain/KeyStore
|
||||
*
|
||||
* @param service The key to kill.
|
||||
*/
|
||||
const loadMmkvEncryptionKey = async function (): Promise<string | undefined> {
|
||||
try {
|
||||
const result = await _Keychain.getGenericPassword({
|
||||
service: KeyChainServiceName.MMKV,
|
||||
authenticationPrompt: {
|
||||
title: 'Authentication required',
|
||||
subtitle: '',
|
||||
description: 'Your Minibits wallet data is encrypted. Please authenticate to get access.',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
})
|
||||
|
||||
if (result) {
|
||||
const key = result.password
|
||||
return key
|
||||
const removeNostrKeypair = async function (): Promise<boolean> {
|
||||
try {
|
||||
const result = await _Keychain.resetGenericPassword({
|
||||
service: KeyChainServiceName.NOSTR
|
||||
})
|
||||
log.trace('[removeNostrKeypair]', 'Removed nostr keypair.')
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
|
||||
log.trace('Did not find existing encryptionKey in the KeyChain')
|
||||
return undefined
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const generateMmkvEncryptionKey = (): string => {
|
||||
try {
|
||||
const keyLength = 32 // Length of the encryption key in bytes
|
||||
const encryptionKey = QuickCrypto.randomBytes(keyLength)
|
||||
const uint8Array = new Uint8Array(encryptionKey)
|
||||
const stringKey = fromByteArray(uint8Array)
|
||||
const base64Key = btoa(stringKey)
|
||||
|
||||
log.trace('New Base64 encryptionKey created:', base64Key)
|
||||
|
||||
return base64Key
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message, [e])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Loads keypair from KeyChain/KeyStore
|
||||
*
|
||||
@@ -148,66 +288,87 @@ const loadMmkvEncryptionKey = async function (): Promise<string | undefined> {
|
||||
* @param service KeyChainServiceName.
|
||||
*/
|
||||
const saveMmkvEncryptionKey = async function (
|
||||
key: string,
|
||||
): Promise<_Keychain.Result | false> {
|
||||
try {
|
||||
const result = await _Keychain.setGenericPassword(
|
||||
KeyChainServiceName.MMKV,
|
||||
key,
|
||||
{
|
||||
service: KeyChainServiceName.MMKV,
|
||||
accessControl: _Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE
|
||||
}
|
||||
)
|
||||
|
||||
log.trace('Saved encryptionKey to the KeyChain')
|
||||
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes keypair from KeyChain/KeyStore
|
||||
*
|
||||
* @param service The key to kill.
|
||||
*/
|
||||
const removeNostrKeypair = async function (): Promise<boolean> {
|
||||
try {
|
||||
const result = await _Keychain.resetGenericPassword({
|
||||
service: KeyChainServiceName.NOSTR
|
||||
})
|
||||
log.trace('Removed nostr keypair', {}, 'removeNostrKeypair')
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes keypair from KeyChain/KeyStore
|
||||
*
|
||||
* @param service The key to kill.
|
||||
*/
|
||||
const removeMmkvEncryptionKey = async function (): Promise<boolean> {
|
||||
key: string,
|
||||
): Promise<_Keychain.Result | false> {
|
||||
try {
|
||||
const result = await _Keychain.resetGenericPassword({
|
||||
service: KeyChainServiceName.MMKV
|
||||
})
|
||||
|
||||
log.trace('Removed mmkv key', {}, 'removeMmkvEncryptionKey')
|
||||
const result = await _Keychain.setGenericPassword(
|
||||
KeyChainServiceName.MMKV,
|
||||
key,
|
||||
{
|
||||
service: KeyChainServiceName.MMKV,
|
||||
accessControl: _Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE
|
||||
}
|
||||
)
|
||||
|
||||
log.trace('[saveMmkvEncryptionKey]', 'Saved encryptionKey to the KeyChain.')
|
||||
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads keypair from KeyChain/KeyStore
|
||||
*
|
||||
*
|
||||
*/
|
||||
const loadMmkvEncryptionKey = async function (): Promise<string | undefined> {
|
||||
try {
|
||||
const result = await _Keychain.getGenericPassword({
|
||||
service: KeyChainServiceName.MMKV,
|
||||
authenticationPrompt: {
|
||||
title: 'Authentication required',
|
||||
subtitle: '',
|
||||
description: 'Your Minibits wallet data is encrypted. Please authenticate to get access.',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
})
|
||||
|
||||
export const KeyChain = {
|
||||
if (result) {
|
||||
const key = result.password
|
||||
return key
|
||||
}
|
||||
|
||||
log.trace('[loadMmkvEncryptionKey]', 'Did not find existing encryptionKey in the KeyChain.')
|
||||
return undefined
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes key from KeyChain/KeyStore
|
||||
*
|
||||
* @param service The key to kill.
|
||||
*/
|
||||
const removeMmkvEncryptionKey = async function (): Promise<boolean> {
|
||||
try {
|
||||
const result = await _Keychain.resetGenericPassword({
|
||||
service: KeyChainServiceName.MMKV
|
||||
})
|
||||
|
||||
log.trace('[removeMmkvEncryptionKey]', 'Removed mmkv key.')
|
||||
return result
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.KEYCHAIN_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const KeyChain = {
|
||||
getSupportedBiometryType,
|
||||
|
||||
generateMnemonic,
|
||||
saveMnemonic,
|
||||
loadMnemonic,
|
||||
removeMnemonic,
|
||||
|
||||
saveSeed,
|
||||
loadSeed,
|
||||
removeSeed,
|
||||
|
||||
generateNostrKeyPair,
|
||||
saveNostrKeyPair,
|
||||
loadNostrKeyPair,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MinibitsClient } from './minibitsService'
|
||||
export type LNURLPayParams = JSLNURLPayParams & {address?: string}
|
||||
|
||||
export type LnurlParamsResult = {
|
||||
lnurlParams: LNURLWithdrawParams | LNURLPayParams
|
||||
lnurlParams: LNURLWithdrawParams | LNURLPayParams & {address: string}
|
||||
encodedInvoice?: string
|
||||
}
|
||||
|
||||
@@ -89,14 +89,14 @@ const getLnurlAddressParams = async (lnurlAddress: string) => {
|
||||
const name = LnurlUtils.getNameFromLnurlAddress(lnurlAddress)
|
||||
|
||||
if(!domain || !name) {
|
||||
throw new AppError(Err.NOTFOUND_ERROR, 'Could not get lightning address name or domain', {caller: 'getLnurlAddressParams'})
|
||||
throw new AppError(Err.NOTFOUND_ERROR, 'Could not get lightning address name or domain', {caller: 'getLnurlAddressParams', lnurlAddress })
|
||||
}
|
||||
|
||||
const url = `https://${domain}/.well-known/lnurlp/${name}`
|
||||
const method = 'GET'
|
||||
const headers = MinibitsClient.getPublicHeaders()
|
||||
|
||||
const lnurlParams = await MinibitsClient.fetchApi(url, {
|
||||
const lnurlParams: LNURLPayParams & LNURLResponse & {address?: string} = await MinibitsClient.fetchApi(url, {
|
||||
method,
|
||||
headers,
|
||||
})
|
||||
|
||||
@@ -706,11 +706,14 @@ const receive = async function (
|
||||
mintsStore.addMint(newMint)
|
||||
}
|
||||
|
||||
const proofsCounter = mintsStore.currentProofsCounterValue(mintToReceive)
|
||||
|
||||
// Now we ask all mints to get fresh outputs for their tokenEntries, and create from them new proofs
|
||||
// 0.8.0-rc3 implements multimints receive however CashuMint constructor still expects single mintUrl
|
||||
const {updatedToken, errorToken, newKeys} = await MintClient.receiveFromMint(
|
||||
const {updatedToken, errorToken, newKeys, errors} = await MintClient.receiveFromMint(
|
||||
mintToReceive,
|
||||
encodedToken as string,
|
||||
proofsCounter
|
||||
)
|
||||
|
||||
if(newKeys) {_updateMintKeys(mintToReceive, newKeys)}
|
||||
@@ -732,27 +735,31 @@ const receive = async function (
|
||||
|
||||
if (errorToken && errorToken.token.length > 0) {
|
||||
amountWithErrors += CashuUtils.getTokenAmounts(errorToken as Token).totalAmount
|
||||
log.warn('[receive]', 'receiveToken amountWithErrors', amountWithErrors)
|
||||
log.warn('[receive]', 'receiveToken amountWithErrors', amountWithErrors, errors)
|
||||
}
|
||||
|
||||
if (amountWithErrors === amountToReceive) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Received ecash token is not valid and can not be redeemed.',
|
||||
'Ecash could not be redeemed.',
|
||||
{caller: 'receive', message: errors?.length ? errors[0]?.message : undefined}
|
||||
)
|
||||
}
|
||||
|
||||
let receivedAmount = 0
|
||||
|
||||
for (const entry of updatedToken.token) {
|
||||
// create ProofModel instances and store them into the proofsStore
|
||||
_addCashuProofs(
|
||||
const { addedProofs, addedAmount } = _addCashuProofs(
|
||||
entry.proofs,
|
||||
entry.mint,
|
||||
transactionId as number
|
||||
)
|
||||
|
||||
receivedAmount += addedAmount
|
||||
}
|
||||
|
||||
// This should be amountToReceive - amountWithErrors but let's set it from updated token
|
||||
const receivedAmount = CashuUtils.getTokenAmounts(updatedToken as Token).totalAmount
|
||||
// const receivedAmount = CashuUtils.getTokenAmounts(updatedToken as Token).totalAmount
|
||||
log.debug('[receive]', `Received amount: ${receivedAmount}`)
|
||||
|
||||
// Update tx amount if full amount was not received
|
||||
@@ -995,11 +1002,14 @@ const receiveOfflineComplete = async function (
|
||||
mintsStore.addMint(newMint)
|
||||
}
|
||||
|
||||
const proofsCounter = mintsStore.currentProofsCounterValue(mintToReceive)
|
||||
|
||||
// Now we ask all mints to get fresh outputs for their tokenEntries, and create from them new proofs
|
||||
// 0.8.0-rc3 implements multimints receive however CashuMint constructor still expects single mintUrl
|
||||
const {updatedToken, errorToken, newKeys} = await MintClient.receiveFromMint(
|
||||
const {updatedToken, errorToken, newKeys, errors} = await MintClient.receiveFromMint(
|
||||
tokenMints[0],
|
||||
encodedToken as string,
|
||||
proofsCounter
|
||||
)
|
||||
|
||||
let amountWithErrors = 0
|
||||
@@ -1012,21 +1022,25 @@ const receiveOfflineComplete = async function (
|
||||
if (amountWithErrors === transaction.amount) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Received ecash token is not valid and can not be redeemed.',
|
||||
'Ecash could not be redeemed.',
|
||||
{caller: 'receiveOfflineComplete', message: errors?.length ? errors[0]?.message : undefined}
|
||||
)
|
||||
}
|
||||
|
||||
let receivedAmount = 0
|
||||
|
||||
for (const entry of updatedToken.token) {
|
||||
// create ProofModel instances and store them into the proofsStore
|
||||
_addCashuProofs(
|
||||
const { addedProofs, addedAmount } = _addCashuProofs(
|
||||
entry.proofs,
|
||||
entry.mint,
|
||||
transaction.id as number
|
||||
)
|
||||
|
||||
receivedAmount += addedAmount
|
||||
}
|
||||
|
||||
// This should be amountToReceive - amountWithErrors but let's set it from updated token
|
||||
const receivedAmount = CashuUtils.getTokenAmounts(updatedToken as Token).totalAmount
|
||||
// const receivedAmount = CashuUtils.getTokenAmounts(updatedToken as Token).totalAmount
|
||||
log.debug('[receiveOfflineComplete]', 'Received amount', receivedAmount)
|
||||
|
||||
// Update tx amount if full amount was not received
|
||||
@@ -1163,48 +1177,40 @@ const _sendFromMint = async function (
|
||||
)
|
||||
log.debug('[_sendFromMint]', 'proofsToSendFrom', proofsToSendFrom)
|
||||
|
||||
const proofsCounter = mintsStore.currentProofsCounterValue(mintUrl)
|
||||
|
||||
// if split to required denominations was necessary, this gets it done with the mint and we get the return
|
||||
const {returnedProofs, proofsToSend, newKeys} = await MintClient.sendFromMint(
|
||||
mintUrl,
|
||||
amountToSend,
|
||||
proofsToSendFrom,
|
||||
proofsCounter
|
||||
)
|
||||
|
||||
log.debug('[_sendFromMint]', 'returnedProofs', returnedProofs)
|
||||
log.debug('[_sendFromMint]', 'proofsToSend', proofsToSend)
|
||||
// log.debug('[_sendFromMint]', 'returnedProofs', returnedProofs)
|
||||
// log.debug('[_sendFromMint]', 'proofsToSend', proofsToSend)
|
||||
|
||||
if (newKeys) {_updateMintKeys(mintUrl, newKeys)}
|
||||
|
||||
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
|
||||
for (const proof of proofsToSend) {
|
||||
if (isStateTreeNode(proof)) {
|
||||
proof.setTransactionId(transactionId)
|
||||
proof.setMintUrl(mintUrl)
|
||||
} else {
|
||||
;(proof as Proof).tId = transactionId
|
||||
;(proof as Proof).mintUrl = mintUrl
|
||||
}
|
||||
}
|
||||
|
||||
// add proofs returned by the mint after the split
|
||||
if (returnedProofs.length > 0) {
|
||||
// these should be fresh proofs from the mint but let's be defensive and cover the case that we over-send proofs while matching denominations
|
||||
for (const proof of returnedProofs) {
|
||||
if (isStateTreeNode(proof)) {
|
||||
proof.setTransactionId(transactionId)
|
||||
proof.setMintUrl(mintUrl)
|
||||
} else {
|
||||
;(proof as Proof).tId = transactionId
|
||||
;(proof as Proof).mintUrl = mintUrl
|
||||
}
|
||||
}
|
||||
|
||||
proofsStore.addProofs(returnedProofs)
|
||||
const { addedProofs, addedAmount } = _addCashuProofs(
|
||||
returnedProofs,
|
||||
mintUrl,
|
||||
transactionId
|
||||
)
|
||||
}
|
||||
|
||||
// remove used proofs and move sent proofs to pending
|
||||
proofsStore.removeProofs(proofsToSendFrom)
|
||||
proofsStore.addProofs(proofsToSend, true) // pending true
|
||||
|
||||
// these might be original proofToSendFrom if they matched the exact amount and split was not necessary
|
||||
const { addedProofs, addedAmount } = _addCashuProofs(
|
||||
proofsToSend,
|
||||
mintUrl,
|
||||
transactionId,
|
||||
true
|
||||
)
|
||||
|
||||
// Clean private properties to not to send them out. This returns plain js array, not model objects.
|
||||
const cleanedProofsToSend = proofsToSend.map(proof => {
|
||||
@@ -1450,6 +1456,8 @@ const transfer = async function (
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
const proofsCounter = mintsStore.currentProofsCounterValue(mintUrl as string)
|
||||
|
||||
// Use prepared proofs to settle with the mint the payment of the invoice on wallet behalf
|
||||
const {feeSavedProofs, isPaid, preimage, newKeys} =
|
||||
await MintClient.payLightningInvoice(
|
||||
@@ -1457,6 +1465,7 @@ const transfer = async function (
|
||||
encodedInvoice,
|
||||
proofsToPay,
|
||||
estimatedFee,
|
||||
proofsCounter
|
||||
)
|
||||
|
||||
if (newKeys) {_updateMintKeys(mintUrl, newKeys)}
|
||||
@@ -1511,12 +1520,13 @@ const transfer = async function (
|
||||
let finalFee = estimatedFee
|
||||
|
||||
if (feeSavedProofs.length) {
|
||||
|
||||
const {addedAmount: feeSaved} = _addCashuProofs(
|
||||
feeSavedProofs,
|
||||
mintUrl,
|
||||
transactionId
|
||||
)
|
||||
|
||||
|
||||
finalFee = estimatedFee - feeSaved
|
||||
}
|
||||
// Save final fee in db
|
||||
@@ -1612,26 +1622,34 @@ const transfer = async function (
|
||||
const _addCashuProofs = function (
|
||||
proofsToAdd: CashuProof[],
|
||||
mintUrl: string,
|
||||
transactionId: number
|
||||
transactionId: number,
|
||||
isPending: boolean = false
|
||||
): {
|
||||
amountToAdd: number,
|
||||
addedAmount: number
|
||||
addedAmount: number,
|
||||
addedProofs: Proof[]
|
||||
} {
|
||||
// Add internal references
|
||||
for (const proof of proofsToAdd as Proof[]) {
|
||||
proof.tId = transactionId
|
||||
proof.mintUrl = mintUrl
|
||||
if (isStateTreeNode(proof)) {
|
||||
proof.setTransactionId(transactionId)
|
||||
proof.setMintUrl(mintUrl)
|
||||
} else {
|
||||
;(proof as Proof).tId = transactionId
|
||||
;(proof as Proof).mintUrl = mintUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const amountToAdd = CashuUtils.getProofsAmount(proofsToAdd as Proof[])
|
||||
// Creates proper model instances and adds them to the wallet
|
||||
const addedAmount = proofsStore.addProofs(proofsToAdd as Proof[])
|
||||
|
||||
const { addedAmount, addedProofs} = proofsStore.addProofs(proofsToAdd as Proof[], isPending)
|
||||
|
||||
log.trace('[_addCashuProofs]', 'Added proofs to the wallet with amount', { amountToAdd, addedAmount })
|
||||
|
||||
return {
|
||||
amountToAdd,
|
||||
addedAmount
|
||||
addedAmount,
|
||||
addedProofs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1851,10 +1869,14 @@ const checkPendingTopups = async function () {
|
||||
try {
|
||||
for (const pr of paymentRequests) {
|
||||
// claim tokens if invoice is paid
|
||||
|
||||
const proofsCounter = mintsStore.currentProofsCounterValue(pr.mint as string)
|
||||
|
||||
const {proofs, newKeys} = (await MintClient.requestProofs(
|
||||
pr.mint as string,
|
||||
pr.amount,
|
||||
pr.paymentHash,
|
||||
proofsCounter
|
||||
)) as {proofs: Proof[], newKeys: MintKeys}
|
||||
|
||||
if (!proofs || proofs.length === 0) {
|
||||
@@ -1893,7 +1915,7 @@ const checkPendingTopups = async function () {
|
||||
proofs,
|
||||
pr.mint as string,
|
||||
pr.transactionId as number
|
||||
)
|
||||
)
|
||||
|
||||
if (receivedAmount !== pr.amount) {
|
||||
throw new AppError(
|
||||
@@ -1952,7 +1974,7 @@ const _updateMintKeys = function (mintUrl: string, newKeys: MintKeys) {
|
||||
const _formatError = function (e: AppError) {
|
||||
return {
|
||||
name: e.name,
|
||||
message: e.message.slice(0, 150),
|
||||
message: e.message.slice(0, 200),
|
||||
params: e.params || {},
|
||||
} as AppError
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ declare module '@env' {
|
||||
export const MINIBITS_SERVER_API_HOST: string
|
||||
export const MINIBITS_NIP05_DOMAIN: string
|
||||
export const MINIBITS_RELAY_URL: string
|
||||
export const MINIBITS_MINT_URL: string
|
||||
export const JS_BUNDLE_VERSION: string
|
||||
export const COMMIT: string
|
||||
}
|
||||
|
||||
+13
-4
@@ -25,14 +25,19 @@ export const getImageSource = function(img: string) {
|
||||
|
||||
|
||||
export const infoMessage = function(message: string, description?: string) {
|
||||
const backgroundColor = colors.dark.info
|
||||
const backgroundColor = colors.palette.success300
|
||||
const textColor = 'white'
|
||||
|
||||
return showMessage({
|
||||
message,
|
||||
description,
|
||||
backgroundColor,
|
||||
color: 'white',
|
||||
style: {minHeight: spacing.screenHeight * 0.15, borderTopLeftRadius: spacing.medium, borderTopRightRadius: spacing.medium},
|
||||
color: textColor,
|
||||
style: {
|
||||
minHeight: spacing.screenHeight * 0.15,
|
||||
borderTopLeftRadius: spacing.medium,
|
||||
borderTopRightRadius: spacing.medium
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,7 +51,11 @@ export const warningMessage = function(message: string, description?: string) {
|
||||
duration: 3000,
|
||||
backgroundColor,
|
||||
color: 'white',
|
||||
style: {minHeight: spacing.screenHeight * 0.15, borderTopLeftRadius: spacing.medium, borderTopRightRadius: spacing.medium},
|
||||
style: {
|
||||
minHeight: spacing.screenHeight * 0.15,
|
||||
borderTopLeftRadius: spacing.medium,
|
||||
borderTopRightRadius: spacing.medium
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1625,12 +1625,12 @@
|
||||
dependencies:
|
||||
"@noble/hashes" "1.3.2"
|
||||
|
||||
"@noble/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
|
||||
"@noble/hashes@1.3.1", "@noble/hashes@~1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
||||
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
||||
|
||||
"@noble/hashes@1.3.2", "@noble/hashes@~1.3.2":
|
||||
"@noble/hashes@1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
||||
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
||||
@@ -1972,12 +1972,12 @@
|
||||
color "^4.2.3"
|
||||
warn-once "^0.1.0"
|
||||
|
||||
"@scure/base@1.1.1", "@scure/base@~1.1.0":
|
||||
"@scure/base@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
|
||||
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
|
||||
|
||||
"@scure/base@~1.1.2":
|
||||
"@scure/base@~1.1.0", "@scure/base@~1.1.2":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f"
|
||||
integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==
|
||||
@@ -6999,20 +6999,20 @@ react-native-json-tree@^1.3.0:
|
||||
prop-types "^15.7.2"
|
||||
react-base16-styling "^0.8.0"
|
||||
|
||||
react-native-keychain@^8.1.1:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-8.1.1.tgz#3bb5e37946b964a7bcf7df2fe470dd244e01a340"
|
||||
integrity sha512-8fxgeHKwGcL657eAYpdBTkDIxNhbIHI+kyyO0Yac2dgVAN184JoIwQcW2z6snahwDaCObQOu0biLFHnsH+4KSg==
|
||||
react-native-keychain@^8.1.2:
|
||||
version "8.1.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-8.1.2.tgz#34291ae472878e5124d081211af5ede7d810e64f"
|
||||
integrity sha512-bhHEui+yMp3Us41NMoRGtnWEJiBE0g8tw5VFpq4mpmXAx6XJYahuM6K3WN5CsUeUl83hYysSL9oFZNKSTPSvYw==
|
||||
|
||||
react-native-logs@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-logs/-/react-native-logs-5.0.1.tgz#53665919587b499e22f514e50cb6a834cedba1ce"
|
||||
integrity sha512-30aQugeLFfcBEKd6U4I75SKc5q5fb9Qrwv0Vnsy3jwpC716xON9SxRbBEnxoPSDJjzK527r4zi+qYqzig6lPlQ==
|
||||
|
||||
react-native-mmkv@^2.8.0:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.10.1.tgz#1a4e64edbf9561fe81a5ae3cefde17e88dfc669f"
|
||||
integrity sha512-MCrYytUJ3JhSVg7LwVXCj7ExbMHe48FoQdDA1xo1q/M4c1/QyI3M9L9d2dyJUhYPnsSO0+5c2mJICm7v39XA0w==
|
||||
react-native-mmkv@^2.11.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.11.0.tgz#51b9985f6a5c09fe9c16d8c1861cc2901856ace1"
|
||||
integrity sha512-28PdUHjZJmAw3q+8zJDAAdohnZMpDC7WgRUJxACOMkcmJeqS3u5cKS/lSq2bhf1CvaeIiHYHUWiyatUjMRCDQQ==
|
||||
|
||||
react-native-modal@^13.0.1:
|
||||
version "13.0.1"
|
||||
@@ -7035,13 +7035,20 @@ react-native-qrcode-svg@^6.2.0:
|
||||
prop-types "^15.8.0"
|
||||
qrcode "^1.5.1"
|
||||
|
||||
react-native-quick-base64@^2.0.5, react-native-quick-base64@^2.0.6:
|
||||
react-native-quick-base64@^2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/react-native-quick-base64/-/react-native-quick-base64-2.0.6.tgz#be5d02186697dafea9cce6b982ba6a2322f2ce42"
|
||||
integrity sha512-sCHsp4EzOL5OZF+77UfMolhgFDo1nM9TbwNFNExVqOFbMwj13KTGwStwjM9diunCHR9hKEt4LEGf9Afq3FOXzA==
|
||||
dependencies:
|
||||
base64-js "^1.5.1"
|
||||
|
||||
react-native-quick-base64@^2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/react-native-quick-base64/-/react-native-quick-base64-2.0.7.tgz#70ec863fb5dba8cd858a262f6b901dbbfbfdd3df"
|
||||
integrity sha512-QmOon3zXAWFi3KvQVCJjCC7N66rwfl1R4nLPuJ+OVs8nWysvKlaU8mKxe2BV4Ud1nB3nTDgyi2VCJFpjtjJxKw==
|
||||
dependencies:
|
||||
base64-js "^1.5.1"
|
||||
|
||||
react-native-quick-crypto@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-quick-crypto/-/react-native-quick-crypto-0.6.1.tgz#7b89c67c4a5d3669c4491fe7884621c1c74d01bc"
|
||||
|
||||
Reference in New Issue
Block a user