Merge branch 'master' into fix-custom-input-lag

This commit is contained in:
Ojok Emmanuel Nsubuga 2026-06-14 14:27:05 +03:00 committed by GitHub
commit 81cf0011b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 568 additions and 698 deletions

View File

@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout Project
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0 # Ensures the full Git history is
@ -490,7 +490,7 @@ jobs:
BRANCH_NAME: ${{ needs.build.outputs.branch_name }}
steps:
- name: Checkout Project
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set Up Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0

View File

@ -49,7 +49,7 @@ jobs:
- name: Checkout project
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0

View File

@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout project
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: "0"
@ -135,7 +135,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
@ -53,6 +53,7 @@ jobs:
BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
HD_MNEMONIC_OLD: ${{ secrets.HD_MNEMONIC_OLD }}
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
HD_MNEMONIC_BREAD: ${{ secrets.HD_MNEMONIC_BREAD }}
@ -64,7 +65,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
@ -83,6 +84,7 @@ jobs:
BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
HD_MNEMONIC_OLD: ${{ secrets.HD_MNEMONIC_OLD }}
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
HD_MNEMONIC_BREAD: ${{ secrets.HD_MNEMONIC_BREAD }}

View File

@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Free disk space (Ubuntu)
run: |
@ -86,7 +86,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Free disk space (Ubuntu)
run: |

View File

@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@ -168,7 +168,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0

View File

@ -1,4 +1,7 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
// Pin the @babel/runtime version so Metro resolves a single copy instead of
// bundling duplicate helpers, which bloats the bundle.
// See https://github.com/babel/babel/issues/18050
presets: [['module:@react-native/babel-preset', { enableBabelRuntime: '^7.26.0' }]],
plugins: ['react-native-worklets/plugin'],
};

View File

@ -2,4 +2,7 @@
* Let's keep config vars, constants and definitions here
*/
export const groundControlUri: string = 'https://groundcontrol.bluewallet.io/';
export const groundControlUri: string = 'https://groundcontrol.bluewallet.io';
/** bitcoin-payment-push-service base URL, no trailing slash. Empty = disabled. */
export const arkadePaymentPushUri: string = 'https://electrum2.bluewallet.io:444';

View File

@ -26,44 +26,93 @@ export interface TinySecp256k1InterfaceExtended {
signDER(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array;
}
necc.utils.sha256Sync = (...messages: Uint8Array[]): Uint8Array => {
const combinedMessages = messages.reduce((acc, msg) => {
const newArray = new Uint8Array(acc.length + msg.length);
newArray.set(acc);
newArray.set(msg, acc.length);
return newArray;
}, new Uint8Array(0));
return sha256(combinedMessages);
};
// @noble/hashes types differ slightly from @noble/secp256k1 v3 hash slot typings.
necc.hashes.sha256 = sha256 as NonNullable<typeof necc.hashes.sha256>;
necc.hashes.hmacSha256 = ((key: Uint8Array, message: Uint8Array) => hmac(sha256, key, message)) as NonNullable<
typeof necc.hashes.hmacSha256
>;
necc.utils.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]): Uint8Array => {
const combinedMessages = messages.reduce((acc, msg) => {
const newArray = new Uint8Array(acc.length + msg.length);
newArray.set(acc);
newArray.set(msg, acc.length);
return newArray;
}, new Uint8Array(0));
return hmac(sha256, key, combinedMessages);
};
/* const normal = necc.utils._normalizePrivateKey;
// Removed from @noble/secp256k1 v1.7; vendored from noble test vectors.
// @see https://github.com/paulmillr/noble-secp256k1/blob/1.7.2/test/index.ts
type Hex = string | Uint8Array;
type PrivKey = Hex | bigint | number;
necc.utils.privateAdd = (privateKey: PrivKey, tweak: Hex) => {
console.log({ privateKey, tweak });
const p = normal(privateKey);
const t = normal(tweak);
return necc.utils.privateAdd(necc.utils.mod(p + t, necc.CURVE.n));
}; */
const { mod, secretKeyToScalar, numberToBytesBE, bytesToNumberBE, hexToBytes } = necc.etc;
const CURVE_N = necc.Point.CURVE().n;
function pointFromBytes(p: Uint8Array): necc.Point {
if (p.length === 32) {
const prefixed = new Uint8Array(33);
prefixed[0] = 0x02;
prefixed.set(p, 1);
return necc.Point.fromBytes(prefixed);
}
return necc.Point.fromBytes(p);
}
const tweakUtils = {
privateAdd: (privateKey: Hex, tweak: Hex): Uint8Array => {
const p = secretKeyToScalar(typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey);
const t = secretKeyToScalar(typeof tweak === 'string' ? hexToBytes(tweak) : tweak);
return numberToBytesBE(mod(p + t, CURVE_N));
},
privateNegate: (privateKey: Hex): Uint8Array => {
const p = secretKeyToScalar(typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey);
return numberToBytesBE(CURVE_N - p);
},
pointAddScalar: (p: Hex, tweak: Hex, isCompressed?: boolean): Uint8Array => {
const P = typeof p === 'string' ? necc.Point.fromHex(p) : pointFromBytes(p);
const t = secretKeyToScalar(typeof tweak === 'string' ? hexToBytes(tweak) : tweak);
const Q = P.add(necc.Point.BASE.multiply(t));
if (Q.is0()) throw new Error('Tweaked point at infinity');
return Q.toBytes(isCompressed);
},
pointMultiply: (p: Hex, tweak: Hex, isCompressed?: boolean): Uint8Array => {
const P = typeof p === 'string' ? necc.Point.fromHex(p) : pointFromBytes(p);
const tweakBytes = typeof tweak === 'string' ? hexToBytes(tweak) : tweak;
const t = mod(bytesToNumberBE(tweakBytes), CURVE_N);
if (t === 0n) throw new Error('Point at infinity');
return P.multiply(t).toBytes(isCompressed);
},
};
const defaultTrue = (param?: boolean): boolean => param !== false;
function compactToDER(sig: Uint8Array): Uint8Array {
const encodeInt = (bytes: Uint8Array): Uint8Array => {
let i = 0;
while (i < bytes.length - 1 && bytes[i] === 0) i++;
let trimmed = bytes.subarray(i);
if (trimmed[0] >= 0x80) {
const prefixed = new Uint8Array(trimmed.length + 1);
prefixed[0] = 0;
prefixed.set(trimmed, 1);
trimmed = prefixed;
}
const encoded = new Uint8Array(2 + trimmed.length);
encoded[0] = 0x02;
encoded[1] = trimmed.length;
encoded.set(trimmed, 2);
return encoded;
};
const rDer = encodeInt(sig.subarray(0, 32));
const sDer = encodeInt(sig.subarray(32, 64));
const seqLen = rDer.length + sDer.length;
const der = new Uint8Array(2 + seqLen);
der[0] = 0x30;
der[1] = seqLen;
der.set(rDer, 2);
der.set(sDer, 2 + rDer.length);
return der;
}
function throwToNull<Type>(fn: () => Type): Type | null {
try {
return fn();
} catch (e) {
// console.log(e);
return null;
}
}
@ -71,7 +120,8 @@ function throwToNull<Type>(fn: () => Type): Type | null {
function isPoint(p: Uint8Array, xOnly: boolean): boolean {
if ((p.length === 32) !== xOnly) return false;
try {
return !!necc.Point.fromHex(p);
pointFromBytes(p);
return true;
} catch (e) {
return false;
}
@ -79,23 +129,12 @@ function isPoint(p: Uint8Array, xOnly: boolean): boolean {
const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256k1InterfaceBIP32 = {
isPoint: (p: Uint8Array): boolean => isPoint(p, false),
isPrivate: (d: Uint8Array): boolean => {
/* if (
[
'0000000000000000000000000000000000000000000000000000000000000000',
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142',
].includes(d.toString('hex'))
) {
return false;
} */
return necc.utils.isValidPrivateKey(d);
},
isPrivate: (d: Uint8Array): boolean => necc.utils.isValidSecretKey(d),
isXOnlyPoint: (p: Uint8Array): boolean => isPoint(p, true),
xOnlyPointAddTweak: (p: Uint8Array, tweak: Uint8Array): { parity: 0 | 1; xOnlyPubkey: Uint8Array } | null =>
throwToNull(() => {
const P = necc.utils.pointAddScalar(p, tweak, true);
const P = tweakUtils.pointAddScalar(p, tweak, true);
const parity = P[0] % 2 === 1 ? 1 : 0;
return { parity, xOnlyPubkey: P.slice(1) };
}),
@ -104,60 +143,56 @@ const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256
throwToNull(() => necc.getPublicKey(sk, defaultTrue(compressed))),
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array => {
return necc.Point.fromHex(p).toRawBytes(defaultTrue(compressed));
return pointFromBytes(p).toBytes(defaultTrue(compressed));
},
pointMultiply: (a: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null =>
throwToNull(() => necc.utils.pointMultiply(a, tweak, defaultTrue(compressed))),
throwToNull(() => tweakUtils.pointMultiply(a, tweak, defaultTrue(compressed))),
pointAdd: (a: Uint8Array, b: Uint8Array, compressed?: boolean): Uint8Array | null =>
throwToNull(() => {
const A = necc.Point.fromHex(a);
const B = necc.Point.fromHex(b);
return A.add(B).toRawBytes(defaultTrue(compressed));
const A = pointFromBytes(a);
const B = pointFromBytes(b);
return A.add(B).toBytes(defaultTrue(compressed));
}),
pointAddScalar: (p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null =>
throwToNull(() => necc.utils.pointAddScalar(p, tweak, defaultTrue(compressed))),
throwToNull(() => tweakUtils.pointAddScalar(p, tweak, defaultTrue(compressed))),
privateAdd: (d: Uint8Array, tweak: Uint8Array): Uint8Array | null =>
throwToNull(() => {
// console.log({ d, tweak });
if (d.join('') === '00000000000000000000000000000001' && tweak.join('') === '00000000000000000000000000000000') {
return new Uint8Array(d); // make test_ecc happy
}
const ret = necc.utils.privateAdd(d, tweak);
// console.log(ret);
const ret = tweakUtils.privateAdd(d, tweak);
if (ret.join('') === '00000000000000000000000000000000') {
return null;
}
return ret;
}),
privateNegate: (d: Uint8Array): Uint8Array => necc.utils.privateNegate(d),
privateNegate: (d: Uint8Array): Uint8Array => tweakUtils.privateNegate(d),
sign: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array => {
return necc.signSync(h, d, { der: false, extraEntropy: e });
return necc.sign(h, d, { prehash: false, extraEntropy: e });
},
signDER: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array => {
return necc.signSync(h, d, { der: true, extraEntropy: e });
return compactToDER(necc.sign(h, d, { prehash: false, extraEntropy: e }));
},
signSchnorr: (h: Uint8Array, d: Uint8Array, e: Uint8Array = new Uint8Array(32).fill(0x00)): Uint8Array => {
return necc.schnorr.signSync(h, d, e);
return necc.schnorr.sign(h, d, e);
},
verify: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean => {
return necc.verify(signature, h, Q, { strict });
return necc.verify(signature, h, Q, { prehash: false, lowS: strict !== false });
},
verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => {
return necc.schnorr.verifySync(signature, h, Q);
return necc.schnorr.verify(signature, h, Q);
},
};
export default ecc;
// module.exports.ecc = ecc;

View File

@ -8,16 +8,16 @@ import {
Notifications,
} from 'react-native-notifications';
import { checkNotifications, requestNotifications, RESULTS } from 'react-native-permissions';
import type { BoltzReverseSwap } from '@arkade-os/boltz-swap';
import loc from '../loc';
import { groundControlUri } from './constants';
import { arkadePaymentPushUri, groundControlUri } from './constants';
import { fetch } from '../util/fetch';
const PUSH_TOKEN = 'PUSH_TOKEN';
const GROUNDCONTROL_BASE_URI = 'GROUNDCONTROL_BASE_URI';
const NOTIFICATIONS_STORAGE = 'NOTIFICATIONS_STORAGE';
const ANDROID_NOTIFICATION_CHANNEL_ID = 'channel_01';
export const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK_FLAG';
let baseURI = groundControlUri;
const baseURI = groundControlUri;
let notificationSubscriptions: EmitterSubscription[] = [];
let onProcessNotificationsHandler: undefined | (() => void | Promise<void>);
const handledNotificationKeys = new Set<string>();
@ -252,6 +252,29 @@ export const tryToObtainPermissions = async (): Promise<boolean> => {
return false;
}
};
export const enqueueTestPushNotification = async (): Promise<void> => {
const pushToken = await getPushToken();
if (!pushToken?.token || !pushToken?.os) {
throw new Error('No push token available');
}
const response = await fetch(`${baseURI}/enqueue`, {
method: 'POST',
headers: _getHeaders(),
body: JSON.stringify({
type: 5,
token: pushToken.token,
os: pushToken.os,
text: 'Test push notification',
}),
});
if (!response.ok) {
throw new Error(`Enqueue request failed with status ${response.status}: ${response.statusText}`);
}
};
/**
* Submits onchain bitcoin addresses and ln invoice preimage hashes to GroundControl server, so later we could
* be notified if they were paid
@ -327,6 +350,44 @@ export const majorTomToGroundControl = async (addresses: string[], hashes: strin
}
};
/**
* Registers an Ark swap with the bitcoin-payment-push-service so the device is
* pushed when the invoice gets paid. Fire-and-forget: never throws, gated by
* the same opt-out/token rules as majorTomToGroundControl(). The swap's
* preimage is always stripped before leaving the device.
*/
export const registerArkPaymentPush = async (paymentHash: string, label: string, pendingSwap: BoltzReverseSwap): Promise<void> => {
if (!arkadePaymentPushUri) return;
try {
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
if (noAndDontAskFlag === 'true') {
console.warn('User has opted out of notifications.');
return;
}
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) {
return;
}
const response = await fetch(`${arkadePaymentPushUri}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
topic: paymentHash,
label,
swap: { ...pendingSwap, preimage: '' },
}),
});
if (!response.ok) {
throw new Error(`status ${response.status}`);
}
console.log('[ARK] payment push registration ok');
} catch (e: any) {
console.log('[ARK] payment push registration failed:', e?.message ?? e);
}
};
/**
* Returns a permissions object:
* alert: boolean
@ -529,22 +590,6 @@ const configureNotifications = async (onProcessNotifications?: () => void): Prom
}
};
/**
* Validates whether the provided GroundControl URI is valid by pinging it.
*
* @param uri {string}
* @returns {Promise<boolean>} TRUE if valid, FALSE otherwise
*/
export const isGroundControlUriValid = async (uri: string) => {
try {
const response = await fetch(`${uri}/ping`, { headers: _getHeaders() });
const json = await response.json();
return !!json.description;
} catch (_) {
return false;
}
};
export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
export const getPushToken = async (): Promise<TPushToken> => {
@ -676,38 +721,6 @@ export const removeAllDeliveredNotifications = () => {
Notifications.removeAllDeliveredNotifications();
};
export const getDefaultUri = () => {
return groundControlUri;
};
export const saveUri = async (uri: string) => {
try {
baseURI = uri || groundControlUri;
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, baseURI);
} catch (error) {
console.error('Error saving URI:', error);
throw error;
}
};
export const getSavedUri = async () => {
try {
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
if (baseUriStored) {
baseURI = baseUriStored;
}
return baseUriStored;
} catch (e) {
console.error(e);
try {
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri);
} catch (storageError) {
console.error('Failed to reset URI:', storageError);
}
throw e;
}
};
export const isNotificationsEnabled = async () => {
try {
const levels = await getLevels();
@ -757,10 +770,6 @@ export const initializeNotifications = async (onProcessNotifications?: () => voi
return;
}
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
baseURI = baseUriStored || groundControlUri;
console.log('Base URI set to:', baseURI);
setApplicationIconBadgeNumber(0);
// Only check permissions, never request
@ -781,7 +790,5 @@ export const initializeNotifications = async (onProcessNotifications?: () => voi
}
} catch (error) {
console.error('Failed to initialize notifications:', error);
baseURI = groundControlUri;
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
}
};

View File

@ -147,11 +147,10 @@ export class BlueApp {
console.warn('error reading', key, error.message);
console.warn('fallback to realm');
const realmKeyValue = await this.openRealmKeyValue();
const obj = realmKeyValue.objectForPrimaryKey('KeyValue', key); // search for a realm object with a primary key
const obj = realmKeyValue.objectForPrimaryKey<{ key: string; value: string }>('KeyValue', key);
value = obj?.value;
realmKeyValue.close();
if (value) {
// @ts-ignore value.length
console.warn('successfully recovered', value.length, 'bytes from realm for key', key);
return value;
}
@ -547,10 +546,11 @@ export class BlueApp {
(walletToInflate._txs_by_internal_index[tx.index] as Transaction[]).push(transaction);
}
} else {
if (!Array.isArray(walletToInflate._txs_by_external_index)) walletToInflate._txs_by_external_index = [];
walletToInflate._txs_by_external_index = walletToInflate._txs_by_external_index || [];
// Legacy single-address wallets - store under index 0
walletToInflate._txs_by_external_index = walletToInflate._txs_by_external_index || {};
walletToInflate._txs_by_external_index[0] = walletToInflate._txs_by_external_index[0] || [];
const transaction = JSON.parse(tx.tx);
(walletToInflate._txs_by_external_index as Transaction[]).push(transaction);
walletToInflate._txs_by_external_index[0].push(transaction);
}
}
}
@ -559,32 +559,6 @@ export class BlueApp {
const id = wallet.getID();
const walletToSave = ('_hdWalletInstance' in wallet && wallet._hdWalletInstance) || wallet;
if (Array.isArray(walletToSave._txs_by_external_index)) {
// if this var is an array that means its a single-address wallet class, and this var is a flat array
// with transactions
realm.write(() => {
// cleanup all existing transactions for the wallet first
const walletTransactionsToDelete = realm.objects('WalletTransactions').filtered(`walletid = '${id}'`);
realm.delete(walletTransactionsToDelete);
// @ts-ignore walletToSave._txs_by_external_index is array
for (const tx of walletToSave._txs_by_external_index) {
realm.create(
'WalletTransactions',
{
walletid: id,
tx: JSON.stringify(tx),
},
Realm.UpdateMode.Modified,
);
}
});
return;
}
/// ########################################################################################################
if (walletToSave._txs_by_external_index) {
realm.write(() => {
// cleanup all existing transactions for the wallet first
@ -592,16 +566,14 @@ export class BlueApp {
realm.delete(walletTransactionsToDelete);
// insert new ones:
for (const index of Object.keys(walletToSave._txs_by_external_index)) {
// @ts-ignore index is number
const txs = walletToSave._txs_by_external_index[index];
for (const [indexStr, txs] of Object.entries(walletToSave._txs_by_external_index)) {
for (const tx of txs) {
realm.create(
'WalletTransactions',
{
walletid: id,
internal: false,
index: parseInt(index, 10),
index: parseInt(indexStr, 10),
tx: JSON.stringify(tx),
},
Realm.UpdateMode.Modified,
@ -609,16 +581,14 @@ export class BlueApp {
}
}
for (const index of Object.keys(walletToSave._txs_by_internal_index)) {
// @ts-ignore index is number
const txs = walletToSave._txs_by_internal_index[index];
for (const [indexStr, txs] of Object.entries(walletToSave._txs_by_internal_index)) {
for (const tx of txs) {
realm.create(
'WalletTransactions',
{
walletid: id,
internal: true,
index: parseInt(index, 10),
index: parseInt(indexStr, 10),
tx: JSON.stringify(tx),
},
Realm.UpdateMode.Modified,

View File

@ -390,7 +390,7 @@ export class HDSegwitBech32Transaction {
}
}
// @ts-ignore stfu
return { tx, inputs, outputs, fee };
// Non-null assertions are safe here because the while loop always runs at least once (add starts at 0)
return { tx: tx!, inputs: inputs!, outputs: outputs!, fee: fee! };
}
}

View File

@ -11,7 +11,7 @@
* @return {Promise.<Uint8Array>} The random bytes
*/
export async function randomBytes(size: number): Promise<Uint8Array> {
const g: any = globalThis as any;
const g = globalThis as any;
const rnCrypto = g && g.crypto;
if (!rnCrypto || typeof rnCrypto.getRandomValues !== 'function') {
throw new Error('crypto.getRandomValues is not available');

View File

@ -45,9 +45,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
_balances_by_external_index: Record<number, BalanceByIndex>;
_balances_by_internal_index: Record<number, BalanceByIndex>;
// @ts-ignore
_txs_by_external_index: Record<number, Transaction[]>;
// @ts-ignore
_txs_by_internal_index: Record<number, Transaction[]>;
_utxo: any[];
@ -204,70 +202,37 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return child.toWIF();
}
_getNodeAddressByIndex(node: number, index: number): string {
index = index * 1; // cast to int
_getNodeByIndex(node: 0 | 1, index: number): BIP32Interface {
const cachedNode = node === 0 ? this._node0 : this._node1;
if (cachedNode) {
return cachedNode.derive(index);
}
const xpub = this._zpubToXpub(this.getXpub());
const hdNode = bip32.fromBase58(xpub).derive(node);
if (node === 0) {
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
}
if (node === 1) {
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
}
if (node === 0 && !this._node0) {
const xpub = this._zpubToXpub(this.getXpub());
const hdNode = bip32.fromBase58(xpub);
this._node0 = hdNode.derive(node);
}
if (node === 1 && !this._node1) {
const xpub = this._zpubToXpub(this.getXpub());
const hdNode = bip32.fromBase58(xpub);
this._node1 = hdNode.derive(node);
}
let address: string;
if (node === 0) {
// @ts-ignore
address = this._hdNodeToAddress(this._node0.derive(index));
this._node0 = hdNode;
} else {
// tbh the only possible else is node === 1
// @ts-ignore
address = this._hdNodeToAddress(this._node1.derive(index));
this._node1 = hdNode;
}
if (node === 0) {
return (this.external_addresses_cache[index] = address);
} else {
// tbh the only possible else option is node === 1
return (this.internal_addresses_cache[index] = address);
}
return hdNode.derive(index);
}
_getNodePubkeyByIndex(node: number, index: number) {
index = index * 1; // cast to int
_getNodeAddressByIndex(node: 0 | 1, index: number): string {
const cache = node === 0 ? this.external_addresses_cache : this.internal_addresses_cache;
if (node === 0 && !this._node0) {
const xpub = this._zpubToXpub(this.getXpub());
const hdNode = bip32.fromBase58(xpub);
this._node0 = hdNode.derive(node);
}
if (cache[index]) return cache[index]; // cache hit
if (node === 1 && !this._node1) {
const xpub = this._zpubToXpub(this.getXpub());
const hdNode = bip32.fromBase58(xpub);
this._node1 = hdNode.derive(node);
}
const hdNode = this._getNodeByIndex(node, index);
const address = this._hdNodeToAddress(hdNode);
if (node === 0 && this._node0) {
return this._node0.derive(index).publicKey;
}
return (cache[index] = address);
}
if (node === 1 && this._node1) {
return this._node1.derive(index).publicKey;
}
throw new Error('Internal error: this._node0 or this._node1 is undefined');
_getNodePubkeyByIndex(node: 0 | 1, index: number) {
return this._getNodeByIndex(node, index).publicKey;
}
_getExternalAddressByIndex(index: number): string {
@ -424,137 +389,95 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// now, we need to put transactions in all relevant `cells` of internal hashmaps:
// this._txs_by_internal_index, this._txs_by_external_index & this._txs_by_payment_code_index
// address -> index lookup maps; the single pass over transactions below uses them
// to find which cells a transaction belongs to
const externalIndexByAddress = new Map<string, number>();
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
for (const tx of Object.values(txdatas)) {
for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
// this TX is related to our address
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
const { vin: txVin, vout: txVout, ...txRest } = tx;
const clonedTx = {
...txRest,
inputs: txVin.slice(0),
outputs: txVout.slice(0),
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
};
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_external_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
}
}
for (const vout of tx.vout) {
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
// this TX is related to our address
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
const { vin: txVin, vout: txVout, ...txRest } = tx;
const clonedTx = {
...txRest,
inputs: txVin.slice(0),
outputs: txVout.slice(0),
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
};
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_external_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
}
}
}
externalIndexByAddress.set(this._getExternalAddressByIndex(c), c);
}
const internalIndexByAddress = new Map<string, number>();
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
for (const tx of Object.values(txdatas)) {
for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
// this TX is related to our address
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
const { vin: txVin, vout: txVout, ...txRest } = tx;
const clonedTx = {
...txRest,
inputs: txVin.slice(0),
outputs: txVout.slice(0),
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
};
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_internal_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
}
}
for (const vout of tx.vout) {
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
// this TX is related to our address
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
const { vin: txVin, vout: txVout, ...txRest } = tx;
const clonedTx = {
...txRest,
inputs: txVin.slice(0),
outputs: txVout.slice(0),
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
};
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_internal_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
}
}
}
internalIndexByAddress.set(this._getInternalAddressByIndex(c), c);
}
const paymentCodeIndexByAddress = new Map<string, { pc: string; c: number }>();
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
for (const tx of Object.values(txdatas)) {
// since we are iterating PCs who can pay us, we can completely ignore `tx.vin` and only iterate `tx.vout`
for (const vout of tx.vout) {
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getBIP47AddressReceive(pc, c)) !== -1) {
// this TX is related to our address
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
const { vin: txVin, vout: txVout, ...txRest } = tx;
const clonedTx = {
...txRest,
inputs: txVin.slice(0),
outputs: txVout.slice(0),
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
};
paymentCodeIndexByAddress.set(this._getBIP47AddressReceive(pc, c), { pc, c });
}
}
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_payment_code_index[pc][c].length; cc++) {
if (this._txs_by_payment_code_index[pc][c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_payment_code_index[pc][c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_payment_code_index[pc][c].push(clonedTx);
}
}
// per-cell txid -> position lookup, used to replace-or-push a transaction into a cell in constant time
const cellPositionsByTxid = new Map<Transaction[], Map<string, number>>();
const getCellPositions = (cell: Transaction[]): Map<string, number> => {
let positions = cellPositionsByTxid.get(cell);
if (!positions) {
positions = new Map();
for (let cc = 0; cc < cell.length; cc++) positions.set(cell[cc].txid, cc);
cellPositionsByTxid.set(cell, positions);
}
return positions;
};
for (const tx of Object.values(txdatas)) {
// collecting which of our address `cells` this transaction touches:
const externalCells = new Set<number>();
const internalCells = new Set<number>();
const paymentCodeCells = new Map<string, { pc: string; c: number }>();
const matchAddress = (address: string, isVout: boolean) => {
const externalIndex = externalIndexByAddress.get(address);
if (externalIndex !== undefined) externalCells.add(externalIndex);
const internalIndex = internalIndexByAddress.get(address);
if (internalIndex !== undefined) internalCells.add(internalIndex);
if (isVout) {
// since we are iterating PCs who can pay us, we can completely ignore `tx.vin` and only check `tx.vout`
const paymentCodeIndex = paymentCodeIndexByAddress.get(address);
if (paymentCodeIndex) paymentCodeCells.set(address, paymentCodeIndex);
}
};
for (const vin of tx.vin) {
for (const address of vin.addresses ?? []) matchAddress(address, false);
}
for (const vout of tx.vout) {
for (const address of vout.scriptPubKey.addresses ?? []) matchAddress(address, true);
}
if (externalCells.size === 0 && internalCells.size === 0 && paymentCodeCells.size === 0) continue;
// this TX is related to our address(es)
const upsertClone = (cell: Transaction[]) => {
const { vin: txVin, vout: txVout, ...txRest } = tx;
const clonedTx = {
...txRest,
inputs: txVin.slice(0),
outputs: txVout.slice(0),
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
};
// trying to replace tx if it exists already (because it has lower confirmations, for example)
const positions = getCellPositions(cell);
const existingPosition = positions.get(clonedTx.txid);
if (existingPosition !== undefined) {
cell[existingPosition] = clonedTx;
} else {
positions.set(clonedTx.txid, cell.length);
cell.push(clonedTx);
}
};
for (const c of externalCells) {
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
upsertClone(this._txs_by_external_index[c]);
}
for (const c of internalCells) {
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
upsertClone(this._txs_by_internal_index[c]);
}
for (const { pc, c } of paymentCodeCells.values()) {
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
upsertClone(this._txs_by_payment_code_index[pc][c]);
}
}
@ -652,8 +575,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
let lastHistoriesWithUsedAddresses = null;
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
const histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
// @ts-ignore
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
if (AbstractHDElectrumWallet._getTransactionsFromHistories(histories).length > 0) {
// in this particular chunk we have used addresses
lastChunkWithUsedAddressesNum = c;
lastHistoriesWithUsedAddresses = histories;
@ -695,8 +617,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
let lastHistoriesWithUsedAddresses = null;
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
const histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
// @ts-ignore
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
if (AbstractHDElectrumWallet._getTransactionsFromHistories(histories).length > 0) {
// in this particular chunk we have used addresses
lastChunkWithUsedAddressesNum = c;
lastHistoriesWithUsedAddresses = histories;
@ -738,8 +659,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
let lastHistoriesWithUsedAddresses = null;
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
const histories = await BlueElectrum.multiGetHistoryByAddress(generateChunkAddresses(c));
// @ts-ignore
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
if (AbstractHDElectrumWallet._getTransactionsFromHistories(histories).length > 0) {
// in this particular chunk we have used addresses
lastChunkWithUsedAddressesNum = c;
lastHistoriesWithUsedAddresses = histories;

View File

@ -315,7 +315,7 @@ export class AbstractHDWallet extends LegacyWallet {
throw new Error('Not implemented');
}
_getNodePubkeyByIndex(node: number, index: number): Uint8Array | undefined {
_getNodePubkeyByIndex(node: 0 | 1, index: number): Uint8Array | undefined {
throw new Error('Not implemented');
}

View File

@ -27,8 +27,8 @@ export class LegacyWallet extends AbstractWallet {
// @ts-ignore: override
public readonly typeReadable: string;
_txs_by_external_index: Transaction[] = [];
_txs_by_internal_index: Transaction[] = [];
_txs_by_external_index: Record<number, Transaction[]> = {};
_txs_by_internal_index: Record<number, Transaction[]> = {};
constructor(typeReadable?: string) {
super();
@ -344,14 +344,14 @@ export class LegacyWallet extends AbstractWallet {
}
}
this._txs_by_external_index = _txsByExternalIndex;
this._txs_by_external_index = { 0: _txsByExternalIndex };
this._lastTxFetch = +new Date();
}
getTransactions(): Transaction[] {
// a hacky code reuse from electrum HD wallet:
this._txs_by_external_index = this._txs_by_external_index || [];
this._txs_by_internal_index = [];
this._txs_by_external_index = this._txs_by_external_index || {};
this._txs_by_internal_index = {};
const { HDSegwitBech32Wallet } = require('./hd-segwit-bech32-wallet') as {
HDSegwitBech32Wallet: typeof HDSegwitBech32WalletT;

View File

@ -29,6 +29,7 @@ import assert from 'assert';
import ecc from '../../blue_modules/noble_ecc.ts';
import { Measure } from '../measure.ts';
import { deleteArkadeRealm, getArkadeRealm } from '../../blue_modules/arkade-adapters/realm/realmInstance';
import { registerArkPaymentPush } from '../../blue_modules/notifications';
const { bech32m } = require('bech32');
const bip32 = BIP32Factory(ecc);
@ -710,6 +711,8 @@ export class LightningArkWallet extends LightningCustodianWallet {
console.log('Pending swap', result.pendingSwap);
console.log('Preimage', result.preimage);
registerArkPaymentPush(result.paymentHash, memo, result.pendingSwap); // fire-and-forget, never throws
return result.invoice;
}

View File

@ -21,6 +21,7 @@ interface ListItemProps {
subtitleNumberOfLines?: number;
rightTitle?: string;
rightTitleStyle?: StyleProp<TextStyle>;
rightTitleSelectable?: boolean;
rightSubtitle?: string | React.ReactNode;
rightSubtitleStyle?: StyleProp<TextStyle>;
chevron?: boolean;
@ -45,6 +46,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
subtitleNumberOfLines,
rightTitle,
rightTitleStyle,
rightTitleSelectable,
rightSubtitle,
rightSubtitleStyle,
chevron,
@ -112,7 +114,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
{rightTitle || rightSubtitle ? (
<View style={styles.rightColumn}>
{rightTitle ? (
<Text style={rightTitleStyle} numberOfLines={1} accessibilityRole="text">
<Text style={rightTitleStyle} numberOfLines={1} accessibilityRole="text" selectable={rightTitleSelectable}>
{rightTitle}
</Text>
) : null}

View File

@ -216,24 +216,11 @@ const QRCode: React.FC<QRCodeProps> = ({
const gradFill = `url(#${GRADIENT_ID})`;
const finderShapes: React.ReactElement[] = [];
const outerR = 2 * cell;
const holeR = 1.25 * cell;
const dotR = 0.9 * cell;
finderOrigins.forEach(([fr, fc], i) => {
const x = (fc + 1) * cell;
const y = (fr + 1) * cell;
finderShapes.push(
<Rect
key={`finder-frame-${i}`}
testID="qr-finder-frame"
x={x}
y={y}
width={7 * cell}
height={7 * cell}
rx={outerR}
ry={outerR}
fill={gradFill}
/>,
<Rect key={`finder-frame-${i}`} testID="qr-finder-frame" x={x} y={y} width={7 * cell} height={7 * cell} fill={gradFill} />,
<Rect
key={`finder-hole-${i}`}
testID="qr-finder-hole"
@ -241,8 +228,6 @@ const QRCode: React.FC<QRCodeProps> = ({
y={y + cell}
width={5 * cell}
height={5 * cell}
rx={holeR}
ry={holeR}
fill={BACKGROUND}
/>,
<Rect
@ -252,8 +237,6 @@ const QRCode: React.FC<QRCodeProps> = ({
y={y + 2 * cell}
width={3 * cell}
height={3 * cell}
rx={dotR}
ry={dotR}
fill={gradFill}
/>,
);
@ -277,16 +260,7 @@ const QRCode: React.FC<QRCodeProps> = ({
{finderShapes}
{isLogoRendered && logoCells > 0 && (
<>
<Rect
testID="qr-logo-backdrop"
x={backdropX}
y={backdropY}
width={backdropSize}
height={backdropSize}
rx={cell * 0.5}
ry={cell * 0.5}
fill={LOGO_BACKGROUND}
/>
<Rect testID="qr-logo-backdrop" x={backdropX} y={backdropY} width={backdropSize} height={backdropSize} fill={LOGO_BACKGROUND} />
<SvgImage
testID="qr-logo-image"
href={require('../img/qr-code.png')}

View File

@ -515,15 +515,7 @@ interface WalletsCarouselProps extends Partial<FlatListProps<any>> {
animateChanges?: boolean;
}
type FlatListRefType = FlatList<any> & {
scrollToEnd(params?: { animated?: boolean | null }): void;
scrollToIndex(params: { animated?: boolean | null; index: number; viewOffset?: number; viewPosition?: number }): void;
scrollToItem(params: { animated?: boolean | null; item: TWallet; viewPosition?: number }): void;
scrollToOffset(params: { animated?: boolean | null; offset: number }): void;
recordInteraction(): void;
flashScrollIndicators(): void;
getNativeScrollRef(): View;
};
export type CarouselListRefType = FlatList<TWallet>;
const styles = StyleSheet.create({
listHeaderSeparator: {
@ -534,7 +526,7 @@ const styles = StyleSheet.create({
const ListHeaderSeparator = () => <View style={styles.listHeaderSeparator} />;
const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props, ref) => {
const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((props, ref) => {
const {
horizontal = true,
data,
@ -569,7 +561,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isInitialMount = useRef(true);
const flatListRef = useRef<FlatList<any>>(null);
const flatListRef = useRef<FlatList<TWallet>>(null);
const walletRefs = useRef<Record<string, React.MutableRefObject<View | null>>>({});
const { sizeClass } = useSizeClass();

View File

@ -288,7 +288,6 @@
"general": "General",
"general_continuity": "Continuity",
"general_continuity_e": "When enabled, you will be able to view selected wallets, and transactions, using your other Apple iCloud connected devices.",
"groundcontrol_explanation": "GroundControl is a free, open-source push notifications server for Bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallets infrastructure. Leave blank to use GroundControls default server.",
"header": "Settings",
"language": "Language",
"last_updated": "Last Updated",
@ -304,7 +303,6 @@
"network_broadcast": "Broadcast Transaction",
"network_electrum": "Electrum Server",
"electrum_suggested_description": "When a preferred server is not set, a suggested server will be selected for use at random.",
"not_a_valid_uri": "Invalid URI",
"notifications": "Notifications",
"open_link_in_explorer": "Open link in explorer",
"password": "Password",
@ -322,7 +320,6 @@
"push_notifications_explanation": "By enabling notifications, your device token will be sent to the server, along with wallet addresses and transaction IDs for all wallets and transactions made after enabling notifications. The device token is used to send notifications, and the wallet information allows us to notify you about incoming Bitcoin or transaction confirmations.\n\nOnly information from after you enable notifications is transmitted—nothing from before is collected.\n\nDisabling notifications will remove all of this information from the server. Additionally, deleting a wallet from the app will also remove its associated information from the server.",
"selfTest": "Self-Test",
"save": "Save",
"saved": "Saved",
"success_transaction_broadcasted": "Your transaction has been successfully broadcasted!",
"total_balance": "Total Balance",
"total_balance_explanation": "Display the total balance of all your wallets on your home screen widgets.",

202
package-lock.json generated
View File

@ -10,15 +10,15 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@arkade-os/boltz-swap": "0.3.37",
"@arkade-os/sdk": "0.4.32",
"@arkade-os/boltz-swap": "0.3.38",
"@arkade-os/sdk": "0.4.33",
"@babel/preset-env": "7.29.5",
"@bugsnag/react-native": "8.9.0",
"@bugsnag/source-maps": "2.3.3",
"@keystonehq/bc-ur-registry": "0.7.1",
"@ngraveio/bc-ur": "1.1.13",
"@noble/hashes": "1.3.3",
"@noble/secp256k1": "1.6.3",
"@noble/secp256k1": "3.1.0",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/cli": "20.1.3",
@ -26,11 +26,11 @@
"@react-native-community/cli-platform-ios": "20.1.3",
"@react-native-documents/picker": "12.0.1",
"@react-native-vector-icons/entypo": "13.1.1",
"@react-native-vector-icons/fontawesome": "13.1.1",
"@react-native-vector-icons/fontawesome6": "13.1.1",
"@react-native-vector-icons/ionicons": "13.1.1",
"@react-native-vector-icons/material-design-icons": "13.1.1",
"@react-native-vector-icons/material-icons": "13.1.1",
"@react-native-vector-icons/fontawesome": "13.1.2",
"@react-native-vector-icons/fontawesome6": "13.1.2",
"@react-native-vector-icons/ionicons": "13.1.2",
"@react-native-vector-icons/material-design-icons": "13.1.2",
"@react-native-vector-icons/material-icons": "13.1.2",
"@react-native/babel-preset": "0.85.3",
"@react-native/codegen": "0.85.3",
"@react-native/gradle-plugin": "0.85.3",
@ -58,7 +58,7 @@
"coinselect": "github:BlueWallet/coinselect#35f8038",
"crypto-browserify": "3.12.1",
"crypto-js": "4.2.0",
"dayjs": "1.11.20",
"dayjs": "1.11.21",
"detox": "20.51.3",
"ecpair": "3.0.1",
"electrum-client": "github:BlueWallet/rn-electrum-client#83420b8",
@ -115,7 +115,6 @@
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
"@jest/reporters": "^27.5.1",
"@react-native/eslint-config": "^0.85.3",
@ -180,12 +179,12 @@
}
},
"node_modules/@arkade-os/boltz-swap": {
"version": "0.3.37",
"resolved": "https://registry.npmjs.org/@arkade-os/boltz-swap/-/boltz-swap-0.3.37.tgz",
"integrity": "sha512-wP4daP/sDpUahmivaIZC8Lfvqz4lhQMWM1R8/Ib5x7NMS6k++FSs4KKQ6wjPKpweF8ULilsJdorhmLpNlEba6A==",
"version": "0.3.38",
"resolved": "https://registry.npmjs.org/@arkade-os/boltz-swap/-/boltz-swap-0.3.38.tgz",
"integrity": "sha512-BVbyw9Fj+1eQn771t0ZO9uW7E1BgViAPLFddb4pnW9p3rM9fCIdWEs2ZrjPnq70leDdhrUxRy++cJuK7zFThuA==",
"license": "MIT",
"dependencies": {
"@arkade-os/sdk": "0.4.32",
"@arkade-os/sdk": "0.4.33",
"@noble/curves": "2.0.1",
"@noble/hashes": "2.0.1",
"@scure/base": "2.0.0",
@ -208,6 +207,8 @@
},
"node_modules/@arkade-os/boltz-swap/node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
@ -217,9 +218,9 @@
}
},
"node_modules/@arkade-os/sdk": {
"version": "0.4.32",
"resolved": "https://registry.npmjs.org/@arkade-os/sdk/-/sdk-0.4.32.tgz",
"integrity": "sha512-we7eNPuuW9PWRS/B4Nlw5MHXTgJ7CuQzbdSrisH0u3P2PPQd/0FbSspEW/OQRNjMrJl+29zAEKN5kswy9MTjxA==",
"version": "0.4.33",
"resolved": "https://registry.npmjs.org/@arkade-os/sdk/-/sdk-0.4.33.tgz",
"integrity": "sha512-EvfmDhSyAiZ7DW89o5D1N4woDEFMfZLHXi/zh9C1xKlPHB2PCezEkHpVe51lNF0Vx3rgkf6bx54QXoGOvg1p9A==",
"license": "MIT",
"dependencies": {
"@bitcoinerlab/descriptors-scure": "3.1.7",
@ -584,7 +585,6 @@
},
"node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
"version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
@ -599,7 +599,6 @@
},
"node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -613,7 +612,6 @@
},
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -629,7 +627,6 @@
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz",
"integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
@ -644,7 +641,6 @@
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
@ -660,7 +656,6 @@
},
"node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
@ -688,7 +683,6 @@
},
"node_modules/@babel/plugin-proposal-private-property-in-object": {
"version": "7.21.0-placeholder-for-preset-env.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -782,7 +776,6 @@
},
"node_modules/@babel/plugin-syntax-import-assertions": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
@ -796,7 +789,6 @@
},
"node_modules/@babel/plugin-syntax-import-attributes": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
@ -950,7 +942,6 @@
},
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
"version": "7.18.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@ -1008,7 +999,6 @@
},
"node_modules/@babel/plugin-transform-block-scoped-functions": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1049,7 +1039,6 @@
},
"node_modules/@babel/plugin-transform-class-static-block": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.28.6",
@ -1082,7 +1071,6 @@
},
"node_modules/@babel/plugin-transform-computed-properties": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
@ -1111,7 +1099,6 @@
},
"node_modules/@babel/plugin-transform-dotall-regex": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
@ -1126,7 +1113,6 @@
},
"node_modules/@babel/plugin-transform-duplicate-keys": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1140,7 +1126,6 @@
},
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
"version": "7.29.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
@ -1155,7 +1140,6 @@
},
"node_modules/@babel/plugin-transform-dynamic-import": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1169,7 +1153,6 @@
},
"node_modules/@babel/plugin-transform-explicit-resource-management": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
@ -1184,7 +1167,6 @@
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
@ -1198,7 +1180,6 @@
},
"node_modules/@babel/plugin-transform-export-namespace-from": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1240,7 +1221,6 @@
},
"node_modules/@babel/plugin-transform-function-name": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.27.1",
@ -1256,7 +1236,6 @@
},
"node_modules/@babel/plugin-transform-json-strings": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
@ -1270,7 +1249,6 @@
},
"node_modules/@babel/plugin-transform-literals": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1284,7 +1262,6 @@
},
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
@ -1298,7 +1275,6 @@
},
"node_modules/@babel/plugin-transform-member-expression-literals": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1312,7 +1288,6 @@
},
"node_modules/@babel/plugin-transform-modules-amd": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.27.1",
@ -1343,7 +1318,6 @@
"version": "7.29.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
"integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.28.6",
@ -1360,7 +1334,6 @@
},
"node_modules/@babel/plugin-transform-modules-umd": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.27.1",
@ -1389,7 +1362,6 @@
},
"node_modules/@babel/plugin-transform-new-target": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1416,7 +1388,6 @@
},
"node_modules/@babel/plugin-transform-numeric-separator": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
@ -1430,7 +1401,6 @@
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.28.6",
@ -1448,7 +1418,6 @@
},
"node_modules/@babel/plugin-transform-object-super": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
@ -1490,7 +1459,6 @@
},
"node_modules/@babel/plugin-transform-parameters": {
"version": "7.27.7",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1533,7 +1501,6 @@
},
"node_modules/@babel/plugin-transform-property-literals": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1616,7 +1583,6 @@
},
"node_modules/@babel/plugin-transform-regexp-modifiers": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
@ -1631,7 +1597,6 @@
},
"node_modules/@babel/plugin-transform-reserved-words": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1676,7 +1641,6 @@
},
"node_modules/@babel/plugin-transform-spread": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
@ -1691,7 +1655,6 @@
},
"node_modules/@babel/plugin-transform-sticky-regex": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1718,7 +1681,6 @@
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1749,7 +1711,6 @@
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1763,7 +1724,6 @@
},
"node_modules/@babel/plugin-transform-unicode-property-regex": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
@ -1792,7 +1752,6 @@
},
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
@ -1809,7 +1768,6 @@
"version": "7.29.5",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz",
"integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.29.3",
@ -1895,7 +1853,6 @@
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
"integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.8",
@ -1907,7 +1864,6 @@
},
"node_modules/@babel/preset-modules": {
"version": "0.1.6-no-external-plugins",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
@ -3579,14 +3535,13 @@
}
},
"node_modules/@noble/secp256k1": {
"version": "1.6.3",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
"integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
@ -3890,12 +3845,12 @@
}
},
"node_modules/@react-native-vector-icons/common": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/common/-/common-13.0.0.tgz",
"integrity": "sha512-FJ0Ql5UTGVtK0ak4vLTxmhFHadb8NmTk4yOWoggh7UvC2pVQNyJK7L9nIZeIZ0IaVJtKfmKXtBWA0nKqqzQ/FQ==",
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/common/-/common-13.0.1.tgz",
"integrity": "sha512-UPC6L3tW5rXCjBn4kgw9RPURUILIg8tFpEY2uaYwU8aCjEHkywNCMcAO8+PvMCDkR6aICPeHYA0OXvMgrjsF4g==",
"license": "MIT",
"dependencies": {
"find-up": "^7.0.0",
"find-up": "^8.0.0",
"picocolors": "^1.1.1",
"plist": "^3.1.0"
},
@ -3908,6 +3863,7 @@
"peerDependencies": {
"@react-native-vector-icons/get-image": "^13.0.0",
"@react-native/assets-registry": "*",
"expo-font": "*",
"react": "*",
"react-native": "*"
},
@ -3917,36 +3873,38 @@
},
"@react-native/assets-registry": {
"optional": true
},
"expo-font": {
"optional": true
}
}
},
"node_modules/@react-native-vector-icons/common/node_modules/find-up": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz",
"integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-8.0.0.tgz",
"integrity": "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==",
"license": "MIT",
"dependencies": {
"locate-path": "^7.2.0",
"path-exists": "^5.0.0",
"unicorn-magic": "^0.1.0"
"locate-path": "^8.0.0",
"unicorn-magic": "^0.3.0"
},
"engines": {
"node": ">=18"
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@react-native-vector-icons/common/node_modules/locate-path": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
"integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-8.0.0.tgz",
"integrity": "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==",
"license": "MIT",
"dependencies": {
"p-locate": "^6.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@ -3982,15 +3940,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@react-native-vector-icons/common/node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/@react-native-vector-icons/common/node_modules/yocto-queue": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
@ -4026,12 +3975,12 @@
}
},
"node_modules/@react-native-vector-icons/fontawesome": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome/-/fontawesome-13.1.1.tgz",
"integrity": "sha512-GD1eOt1YmkxbUmHZzxpCGMMC3WCif3edo8RKMnv0dlf07KNLktfQDh0mVYJhU4d203oyeTk1E5GWBjNDRw3zWg==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome/-/fontawesome-13.1.2.tgz",
"integrity": "sha512-Pae4/aDhvSd5FNVy6QOfcQ8uhj+fpbIc2WFDaO9jLEkqM0p5tMZt39Mcfw1XosOXQ0eSqJdlYoI2x8vqbdyzXg==",
"license": "MIT",
"dependencies": {
"@react-native-vector-icons/common": "^13.0.0"
"@react-native-vector-icons/common": "^13.0.1"
},
"engines": {
"node": ">= 18.0.0"
@ -4048,12 +3997,12 @@
}
},
"node_modules/@react-native-vector-icons/fontawesome6": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome6/-/fontawesome6-13.1.1.tgz",
"integrity": "sha512-AwZSCk+2dakqzlBEEKwi/FBc6qg4TtGPPyj2OVt0HcA8sy+gMa0u5iW7hao/Fmq3ad0LQz9HTUYUeslH2jS0jA==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome6/-/fontawesome6-13.1.2.tgz",
"integrity": "sha512-oQvQeDE8kSXm3l+oRKQm/Jo4ewR9YdKW2gFDVVl3st1yY5Nml1ZS4m3lTp3a/KehT9w+Uiv2JNn3kG0VOo+AZw==",
"license": "MIT",
"dependencies": {
"@react-native-vector-icons/common": "^13.0.0"
"@react-native-vector-icons/common": "^13.0.1"
},
"engines": {
"node": ">= 18.0.0"
@ -4070,12 +4019,12 @@
}
},
"node_modules/@react-native-vector-icons/ionicons": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/ionicons/-/ionicons-13.1.1.tgz",
"integrity": "sha512-OAIEf7HW5SnDi+YMRR1W/HBwzWmQiQ4msY8aSQRdVisPvbVFvO6vaWJdV33QI2aj1/5lVLh9oKJGcRsSaBzh2Q==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/ionicons/-/ionicons-13.1.2.tgz",
"integrity": "sha512-8TaXKw41MgKADeesrrbUpA3FR81JNy96ogiGRjWgtE1djSEevDsOKMij7Jq/3TfiGaE0prEshU0TcW5qwsf0Ug==",
"license": "MIT",
"dependencies": {
"@react-native-vector-icons/common": "^13.0.0"
"@react-native-vector-icons/common": "^13.0.1"
},
"engines": {
"node": ">= 18.0.0"
@ -4092,12 +4041,12 @@
}
},
"node_modules/@react-native-vector-icons/material-design-icons": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-design-icons/-/material-design-icons-13.1.1.tgz",
"integrity": "sha512-bKkai9GSMOrqIwKskHZuegejgO6bLp7xNgp7YdeLprkEK44/HsATjCpXhwvRPYq9RSHdOvrFFKBIKLZbkpijSw==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-design-icons/-/material-design-icons-13.1.2.tgz",
"integrity": "sha512-Qc8IQCxbnHOk8CvTAb+dLzYgRMbJOLiZ8Up7TRsNixY6EqwPx9/W3DeK5niKtNQ4dIfbALeYz41yyvDM7w7mag==",
"license": "MIT",
"dependencies": {
"@react-native-vector-icons/common": "^13.0.0"
"@react-native-vector-icons/common": "^13.0.1"
},
"engines": {
"node": ">= 18.0.0"
@ -4114,12 +4063,12 @@
}
},
"node_modules/@react-native-vector-icons/material-icons": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-icons/-/material-icons-13.1.1.tgz",
"integrity": "sha512-u13/5ITff+qGBZBnv3QQ+vLNCNgJzxUfXnMnZDK1rHgpUjH6lex3tSORX5XLYbCuaHDW7WFF0cqzoaephYZApg==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-icons/-/material-icons-13.1.2.tgz",
"integrity": "sha512-z8fckMFeYvvVzqfWpsM8AkSFf0pFwlwueKq8/HAKetrZEl3GsK29Mr+sv7me0N6kAl9Z+AaNXqD7gNQpCjkZgg==",
"license": "MIT",
"dependencies": {
"@react-native-vector-icons/common": "^13.0.0"
"@react-native-vector-icons/common": "^13.0.1"
},
"engines": {
"node": ">= 18.0.0"
@ -8077,9 +8026,9 @@
}
},
"node_modules/dayjs": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"version": "1.11.21",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
"license": "MIT"
},
"node_modules/debug": {
@ -9833,7 +9782,6 @@
},
"node_modules/esutils": {
"version": "2.0.3",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@ -17526,6 +17474,18 @@
"ecpair": "3.0.0"
}
},
"node_modules/silent-payments/node_modules/@noble/secp256k1": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.6.3.tgz",
"integrity": "sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/silent-payments/node_modules/@scure/base": {
"version": "1.2.6",
"license": "MIT",
@ -18880,9 +18840,9 @@
}
},
"node_modules/unicorn-magic": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
"integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"license": "MIT",
"engines": {
"node": ">=18"

View File

@ -16,7 +16,6 @@
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
"@jest/reporters": "^27.5.1",
"@react-native/eslint-config": "^0.85.3",
@ -91,18 +90,18 @@
"lint": " npm run tslint && node scripts/find-unused-loc.js && node scripts/find-english-leftovers.js && eslint --ext .js,.ts,.tsx '*.@(js|ts|tsx)' screen 'blue_modules/*.@(js|ts|tsx)' class models loc tests components navigation typings",
"lint:fix": "npm run lint -- --fix",
"lint:quickfix": "git status --porcelain | grep -v '\\.json' | grep -E '\\.js|\\.ts' --color=never | awk '{print $2}' | xargs eslint --fix; exit 0",
"unit": "jest -b -w tests/unit/*"
"unit": "jest -b tests/unit/*"
},
"dependencies": {
"@arkade-os/boltz-swap": "0.3.37",
"@arkade-os/sdk": "0.4.32",
"@arkade-os/boltz-swap": "0.3.38",
"@arkade-os/sdk": "0.4.33",
"@babel/preset-env": "7.29.5",
"@bugsnag/react-native": "8.9.0",
"@bugsnag/source-maps": "2.3.3",
"@keystonehq/bc-ur-registry": "0.7.1",
"@ngraveio/bc-ur": "1.1.13",
"@noble/hashes": "1.3.3",
"@noble/secp256k1": "1.6.3",
"@noble/secp256k1": "3.1.0",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/cli": "20.1.3",
@ -110,11 +109,11 @@
"@react-native-community/cli-platform-ios": "20.1.3",
"@react-native-documents/picker": "12.0.1",
"@react-native-vector-icons/entypo": "13.1.1",
"@react-native-vector-icons/fontawesome": "13.1.1",
"@react-native-vector-icons/fontawesome6": "13.1.1",
"@react-native-vector-icons/ionicons": "13.1.1",
"@react-native-vector-icons/material-design-icons": "13.1.1",
"@react-native-vector-icons/material-icons": "13.1.1",
"@react-native-vector-icons/fontawesome": "13.1.2",
"@react-native-vector-icons/fontawesome6": "13.1.2",
"@react-native-vector-icons/ionicons": "13.1.2",
"@react-native-vector-icons/material-design-icons": "13.1.2",
"@react-native-vector-icons/material-icons": "13.1.2",
"@react-native/babel-preset": "0.85.3",
"@react-native/codegen": "0.85.3",
"@react-native/gradle-plugin": "0.85.3",
@ -142,7 +141,7 @@
"coinselect": "github:BlueWallet/coinselect#35f8038",
"crypto-browserify": "3.12.1",
"crypto-js": "4.2.0",
"dayjs": "1.11.20",
"dayjs": "1.11.21",
"detox": "20.51.3",
"ecpair": "3.0.1",
"electrum-client": "github:BlueWallet/rn-electrum-client#83420b8",

View File

@ -46,7 +46,7 @@ const LNDViewInvoice = () => {
const [isFetchingInvoices, setIsFetchingInvoices] = useState<boolean>(true);
const [invoiceStatusChanged, setInvoiceStatusChanged] = useState<boolean>(false);
const [qrCodeSize, setQRCodeSize] = useState<number>(90);
const fetchInvoiceInterval = useRef<any>(null);
const fetchInvoiceInterval = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
const isModal = useNavigationState(state => state.routeNames[0] === LNDCreateInvoice.routeName);
// Per-swap claim/refund lookup, by the `swap-${id}` prefix mapped onto
@ -179,7 +179,6 @@ const LNDViewInvoice = () => {
fetchInvoiceInterval.current = setInterval(async () => {
if (isFetchingInvoices) {
try {
// @ts-ignore - getUserInvoices is not set on TWallet
const userInvoices: LightningTransaction[] = await wallet.getUserInvoices(20);
// fetching only last 20 invoices
// for invoice that was created just now - that should be enough (it is basically the last one, so limit=1 would be sufficient)

View File

@ -1,7 +1,7 @@
import { RouteProp, StackActions, useIsFocused, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, ScrollView, StyleSheet, View } from 'react-native';
import { ActivityIndicator, ScrollView, StyleSheet, View, TouchableOpacity } from 'react-native';
import presentAlert from '../../components/Alert';
import { DynamicQRCode } from '../../components/DynamicQRCode';
import SaveFileButton from '../../components/SaveFileButton';
@ -23,7 +23,7 @@ type RouteParams = RouteProp<SendDetailsStackParamList, 'PsbtMultisigQRCode'>;
const PsbtMultisigQRCode: React.FC = () => {
const navigation = useExtendedNavigation();
const { colors } = useTheme();
const openScannerButton = useRef<any>(null);
const openScannerButton = useRef<React.ElementRef<typeof TouchableOpacity>>(null);
const { params } = useRoute<RouteParams>();
const { psbtBase64, isShowOpenScanner, walletID } = params;
const [isLoading, setIsLoading] = useState<boolean>(false);

View File

@ -91,7 +91,7 @@ const SendDetails = () => {
const payjoinUrl = route.params?.payjoinUrl;
const isTransactionReplaceable = route.params?.isTransactionReplaceable;
const routeParams = route.params;
const scrollView = useRef<FlatList<any>>(null);
const scrollView = useRef<FlatList<IPaymentDestinations>>(null);
const scrollIndex = useRef(0);
/** Used so we only clear coin-selection (utxos) when the user switches wallet, not on first mount (e.g. Send opened from wallet details with pre-selected UTXOs). */
const prevWalletIdForCoinResetRef = useRef<string | null>(null);
@ -221,9 +221,6 @@ const SendDetails = () => {
}
return updatedAddresses;
});
// @ts-ignore: Fix later
setParams(prevParams => ({ ...prevParams, addRecipientParams: undefined }));
} else {
setAddresses([{ address: '', key: String(Math.random()), unit: amountUnit }]); // key is for the FlatList
}

View File

@ -1,22 +1,19 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Linking, StyleSheet, TextInput, View, Pressable, AppState, Text } from 'react-native';
import { StyleSheet, View, Pressable, AppState, Text } from 'react-native';
import {
getDefaultUri,
getPushToken,
getSavedUri,
getStoredNotifications,
saveUri,
isNotificationsEnabled,
setLevels,
tryToObtainPermissions,
cleanUserOptOutFlag,
isGroundControlUriValid,
checkPermissions,
checkNotificationPermissionStatus,
enqueueTestPushNotification,
NOTIFICATIONS_NO_AND_DONT_ASK_FLAG,
} from '../../blue_modules/notifications';
import { BlueSpacing20 } from '../../components/BlueSpacing';
import presentAlert from '../../components/Alert';
import { BlueSpacing20 } from '../../components/BlueSpacing';
import { Button } from '../../components/Button';
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
import { useTheme } from '../../components/themes';
@ -43,7 +40,6 @@ const NotificationSettings: React.FC = () => {
const [isNotificationsEnabledState, setNotificationsEnabledState] = useState<boolean | undefined>(undefined);
const [tokenInfo, setTokenInfo] = useState('<empty>');
const [URI, setURI] = useState<string | undefined>();
const [tapCount, setTapCount] = useState(0);
const { colors } = useTheme();
@ -139,7 +135,6 @@ const NotificationSettings: React.FC = () => {
await updateNotificationStatus();
}
setURI((await getSavedUri()) ?? getDefaultUri());
setTokenInfo(
'token: ' +
JSON.stringify(await getPushToken()) +
@ -172,25 +167,17 @@ const NotificationSettings: React.FC = () => {
};
}, []);
const save = useCallback(async () => {
const enqueueTestPush = useCallback(async () => {
setIsLoading(true);
try {
if (URI) {
if (await isGroundControlUriValid(URI)) {
await saveUri(URI);
presentAlert({ message: loc.settings.saved });
} else {
presentAlert({ message: loc.settings.not_a_valid_uri });
}
} else {
await saveUri('');
presentAlert({ message: loc.settings.saved });
}
await enqueueTestPushNotification();
} catch (error) {
console.error('Error saving URI:', error);
console.error('Error enqueueing test push:', error);
presentAlert({ message: (error as Error).message });
} finally {
setIsLoading(false);
}
setIsLoading(false);
}, [URI]);
}, []);
const renderDeveloperSettings = useCallback(() => {
if (tapCount < 10) return null;
@ -198,44 +185,9 @@ const NotificationSettings: React.FC = () => {
return (
<View>
<View style={[styles.divider, { backgroundColor: colors.lightBorder ?? colors.borderTopColor }]} />
<SettingsCard style={styles.card}>
<View style={styles.cardContent}>
<Text style={[styles.multilineText, { color: colors.foregroundColor }]}>{loc.settings.groundcontrol_explanation}</Text>
</View>
</SettingsCard>
<SettingsListItem
title="github.com/BlueWallet/GroundControl"
iconName="github"
onPress={() => Linking.openURL('https://github.com/BlueWallet/GroundControl')}
chevron
position="single"
spacingTop
/>
<SettingsCard style={styles.card}>
<View style={styles.cardContent}>
<View
style={[
styles.uri,
{ borderColor: colors.formBorder, borderBottomColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor },
]}
>
<TextInput
placeholder={getDefaultUri()}
value={URI}
onChangeText={setURI}
numberOfLines={1}
style={[styles.uriText, { color: colors.alternativeTextColor }]}
placeholderTextColor="#81868e"
editable={!isLoading}
textContentType="URL"
autoCapitalize="none"
underlineColorAndroid="transparent"
/>
</View>
<BlueSpacing20 />
<Text style={[styles.centered, { color: colors.foregroundColor }]} onPress={() => setTapCount(tapCount + 1)}>
Ground Control to Major Tom
</Text>
@ -248,12 +200,12 @@ const NotificationSettings: React.FC = () => {
</View>
<BlueSpacing20 />
<Button onPress={save} title={loc.settings.save} />
<Button onPress={enqueueTestPush} title="Enqueue test push notification" disabled={isLoading} />
</View>
</SettingsCard>
</View>
);
}, [tapCount, colors, isLoading, URI, tokenInfo, save]);
}, [tapCount, colors, isLoading, tokenInfo, enqueueTestPush]);
const renderPushNotificationsExplanation = useCallback(() => {
return (
@ -375,28 +327,10 @@ const styles = StyleSheet.create({
paddingHorizontal: horizontalPadding,
paddingVertical: isAndroid ? 12 : 10,
},
multilineText: {
lineHeight: 20,
paddingBottom: 10,
},
centered: {
textAlign: 'center',
marginVertical: 4,
},
uri: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
alignItems: 'center',
},
uriText: {
flex: 1,
marginHorizontal: 8,
minHeight: 36,
height: 36,
},
divider: {
marginVertical: isAndroid ? 16 : 12,
height: 0.5,

View File

@ -5,7 +5,7 @@ import { StyleSheet, View, ViewStyle, Animated, ScrollView } from 'react-native'
import { TWallet } from '../../class/wallets/types';
import { Header } from '../../components/Header';
import { useTheme } from '../../components/themes';
import WalletsCarousel from '../../components/WalletsCarousel';
import WalletsCarousel, { CarouselListRefType } from '../../components/WalletsCarousel';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import TotalWalletsBalance from '../../components/TotalWalletsBalance';
@ -94,7 +94,7 @@ const DrawerList: React.FC<DrawerContentComponentProps> = memo((props: DrawerCon
const drawerNavigation = props.navigation;
const [state, dispatch] = useReducer(walletReducer, initialState);
const walletsCarousel = useRef<any>(null);
const walletsCarousel = useRef<CarouselListRefType>(null);
const { wallets, selectedWalletID } = useStorage();
const { colors } = useTheme();
const isFocused = useIsFocused();

View File

@ -78,7 +78,7 @@ const ExportMultisigCoordinationSetup: React.FC = () => {
const { wallets } = useStorage();
const { isPrivacyBlurEnabled } = useSettings();
const wallet: TWallet | undefined = wallets.find(w => w.getID() === walletID);
const dynamicQRCode = useRef<any>(null);
const dynamicQRCode = useRef<DynamicQRCode>(null);
const { colors } = useTheme();
const { enableScreenProtect, disableScreenProtect } = useScreenProtect();

View File

@ -8,7 +8,7 @@ import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import WalletsCarousel from '../../components/WalletsCarousel';
import WalletsCarousel, { CarouselListRefType } from '../../components/WalletsCarousel';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { TWallet } from '../../class/wallets/types';
import { pop } from '../../NavigationService';
@ -35,7 +35,7 @@ const SelectWallet: React.FC = () => {
const { wallets } = useStorage();
const { colors } = useTheme();
const isModal = useNavigationState(state => state.routes.length > 1);
const walletsCarousel = useRef<any>(null);
const walletsCarousel = useRef<CarouselListRefType>(null);
const previousRouteName = useNavigationState(state => state.routes[state.routes.length - 2]?.name);
const [filteredWallets, setFilteredWallets] = useState<TWallet[]>([]);

View File

@ -542,6 +542,7 @@ const WalletDetails: React.FC = () => {
numberOfLines={1}
ellipsizeMode="tail"
testID="WalletNameDisplay"
selectable
>
{walletName}
</Text>
@ -779,7 +780,6 @@ const WalletDetails: React.FC = () => {
containerStyle={stylesHook.listItemContainerBorder}
onPress={navigateToXPub}
title={loc.wallets.details_show_xpub}
chevron
testID="XpubButton"
bottomDivider
/>
@ -789,7 +789,6 @@ const WalletDetails: React.FC = () => {
containerStyle={stylesHook.listItemContainerBorder}
onPress={navigateToSignVerify}
title={loc.addresses.sign_title}
chevron
testID="SignVerify"
bottomDivider={!!(wallet.type === MultisigHDWallet.type)}
/>
@ -840,6 +839,7 @@ const WalletDetails: React.FC = () => {
titleStyle={stylesHook.advancedListItemTitle}
rightTitle={wallet.typeReadable}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
rightTitleSelectable
bottomDivider={
!!(
wallet.type === MultisigHDWallet.type ||
@ -880,6 +880,7 @@ const WalletDetails: React.FC = () => {
isMasterFingerPrintVisible ? (masterFingerprint ?? loc.wallets.import_derivation_loading) : loc.multisig.view
}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
rightTitleSelectable={isMasterFingerPrintVisible}
bottomDivider={!!derivationPath}
/>
)}
@ -890,6 +891,7 @@ const WalletDetails: React.FC = () => {
titleStyle={stylesHook.advancedListItemTitle}
rightTitle={derivationPath}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
rightTitleSelectable
bottomDivider={false}
testID="DerivationPath"
/>

View File

@ -82,7 +82,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
const [displayUnit, setDisplayUnit] = useState(wallet.preferredBalanceUnit);
const [isUnitSwitching, setIsUnitSwitching] = useState(false);
const [isWatchOnlyWarningVisible, setIsWatchOnlyWarningVisible] = useState<boolean>(() => {
return wallet.type === WatchOnlyWallet.type && (wallet as any).isWatchOnlyWarningVisible;
return wallet.type === WatchOnlyWallet.type && (wallet as WatchOnlyWallet).isWatchOnlyWarningVisible;
});
const MAX_FAILURES = 3;
const flatListRef = useRef<FlatList<Transaction>>(null);
@ -172,7 +172,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
}, [wallet, walletID]);
useEffect(() => {
setIsWatchOnlyWarningVisible(wallet.type === WatchOnlyWallet.type && (wallet as any).isWatchOnlyWarningVisible);
setIsWatchOnlyWarningVisible(wallet.type === WatchOnlyWallet.type && (wallet as WatchOnlyWallet).isWatchOnlyWarningVisible);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletID]);
@ -547,7 +547,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
if ('setPreferredBalanceUnit' in wallet) {
wallet.setPreferredBalanceUnit(selectedUnit);
} else {
(wallet as any).preferredBalanceUnit = selectedUnit;
(wallet as TWallet).preferredBalanceUnit = selectedUnit;
}
await saveToDisk();
console.debug('[UnitSwitch] persisted preferred unit', { walletID, unit: selectedUnit });

View File

@ -11,7 +11,7 @@ import presentAlert from '../../components/Alert';
import { FButton, FContainer, FloatButtonsBottomFade } from '../../components/FloatButtons';
import { useTheme } from '../../components/themes';
import { TransactionListItem } from '../../components/TransactionListItem';
import WalletsCarousel, { getWalletCarouselItemWidth } from '../../components/WalletsCarousel';
import WalletsCarousel, { getWalletCarouselItemWidth, CarouselListRefType } from '../../components/WalletsCarousel';
import { useSizeClass, SizeClass } from '../../blue_modules/sizeClass';
import loc from '../../loc';
import ActionSheet from '../ActionSheet';
@ -101,7 +101,7 @@ const WalletsList: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { isLoading } = state;
const { sizeClass, isLarge } = useSizeClass();
const walletsCarousel = useRef<any>(null);
const walletsCarousel = useRef<CarouselListRefType>(null);
const connectionPoll = useContext(ConnectionPollContext);
const currentWalletIndex = useRef<number>(0);
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();

View File

@ -19,7 +19,7 @@
*/
import { closeAllArkadeRealms, __testing__ as realmTesting } from '../../blue_modules/arkade-adapters/realm/realmInstance';
import { __testing__ as walletTesting } from '../../class/wallets/lightning-ark-wallet';
import { LightningArkWallet, __testing__ as walletTesting } from '../../class/wallets/lightning-ark-wallet';
const Realm = require('realm');
@ -81,3 +81,39 @@ export const arkadeMockState = {
Keychain.__mockKeychainHelpers.store.set(service, { username: service, password, service });
},
};
/**
* Tear down a LightningArkWallet after integration tests. Stops SDK background
* loops (ContractWatcher SSE, VtxoManager polling, SwapManager) via dispose()
* before clearing module-private caches.
*/
export async function teardownArkadeWallet(w: LightningArkWallet): Promise<void> {
try {
await w.onDelete();
} catch {
// onDelete already logs and swallows per-namespace errors.
}
}
/** Best-effort dispose of any Arkade SDK runtime still cached module-wide. */
export async function disposeAllArkadeRuntime(): Promise<void> {
for (const ns of Object.keys(walletTesting.staticSwapsCache)) {
const swaps = walletTesting.staticSwapsCache[ns];
try {
if (typeof swaps?.dispose === 'function') await swaps.dispose();
} catch {}
delete walletTesting.staticSwapsCache[ns];
}
for (const ns of Object.keys(walletTesting.staticWalletCache)) {
const sdkWallet = walletTesting.staticWalletCache[ns];
try {
if (typeof sdkWallet?.dispose === 'function') await sdkWallet.dispose();
} catch {}
delete walletTesting.staticWalletCache[ns];
}
walletTesting.initInFlight.clear();
walletTesting.restoreInFlight.clear();
for (const k of Object.keys(walletTesting.boardingLock)) delete walletTesting.boardingLock[k];
closeAllArkadeRealms();
realmTesting.openInFlight.clear();
}

View File

@ -110,3 +110,25 @@ export function installSdkProviderSpies(): void {
export function restoreSdkProviderSpies(): void {
jest.restoreAllMocks();
}
let backgroundLoopSpies: jest.SpiedFunction<any>[] = [];
export function restoreSdkBackgroundLoopStubs(): void {
for (const spy of backgroundLoopSpies) spy.mockRestore();
backgroundLoopSpies = [];
}
/**
* Stub only the SDK background subscriptions that Jest cannot shut down
* cleanly (VtxoManager polling, SwapManager WebSocket, ContractWatcher SSE).
* Real HTTP calls (getInfo, getTransactionHistory, restoreSwaps, etc.) still
* run use in env-gated integration tests that hit production services.
*/
export function installSdkBackgroundLoopStubs(): void {
restoreSdkBackgroundLoopStubs();
backgroundLoopSpies = [
jest.spyOn(VtxoManager.prototype as any, 'initializeSubscription').mockResolvedValue(undefined),
jest.spyOn(SwapManager.prototype as any, 'start').mockResolvedValue(undefined),
jest.spyOn(ContractManager.prototype as any, 'initialize').mockResolvedValue(undefined),
];
}

View File

@ -2,6 +2,8 @@ import assert from 'assert';
import { HDSegwitBech32Wallet } from '../../class/wallets/hd-segwit-bech32-wallet';
import { LightningArkWallet } from '../../class/wallets/lightning-ark-wallet.ts';
import { disposeAllArkadeRuntime, teardownArkadeWallet } from '../helpers/arkadeMocks';
import { installSdkBackgroundLoopStubs, restoreSdkBackgroundLoopStubs } from '../helpers/sdkProviderMocks';
// Ark storage lives in Realm, not AsyncStorage. Realm + Keychain are mocked
// globally by tests/setup.js (per-path Realm + service-keyed Keychain), and
@ -15,29 +17,41 @@ jest.setTimeout(30_000);
const w = new LightningArkWallet();
beforeAll(async () => {
// Install before the env guard: `can generate` runs init() regardless of
// HD_MNEMONIC_OLD, and without the stubs its background loops keep Jest alive.
installSdkBackgroundLoopStubs();
if (!process.env.HD_MNEMONIC_OLD) {
console.error('process.env.HD_MNEMONIC_OLD not set, skipped');
return;
}
w.setSecret('arkade://' + process.env.HD_MNEMONIC_OLD);
await w.init();
await w.restoreSwaps();
});
afterAll(async () => {
await new Promise(resolve => setTimeout(resolve, 3_000)); // sleep
if (process.env.HD_MNEMONIC_OLD) {
await teardownArkadeWallet(w);
}
await disposeAllArkadeRuntime();
restoreSdkBackgroundLoopStubs();
});
describe('LightningArkWallet (integration)', () => {
it('can generate', async () => {
const wGenerated = new LightningArkWallet();
await wGenerated.generate();
try {
await wGenerated.generate();
assert.ok(wGenerated.getSecret().startsWith('arkade://'));
assert.ok(wGenerated.getSecret().startsWith('arkade://'));
const mnemonics = wGenerated.getSecret().replace('arkade://', '');
const hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonics);
assert.ok(hd.validateMnemonic());
const mnemonics = wGenerated.getSecret().replace('arkade://', '');
const hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonics);
assert.ok(hd.validateMnemonic());
} finally {
await teardownArkadeWallet(wGenerated);
}
});
it('can fetch balance', async () => {
@ -70,47 +84,48 @@ describe('LightningArkWallet (integration)', () => {
}
await w.fetchTransactions();
await w.fetchUserInvoices();
const txs = w.getTransactions();
assert.ok(txs.length > 0);
assert.ok(txs.length > 0, 'Should have transaction history from the Ark indexer');
// Find the reverse swap (incoming) transaction
const receiveTx = txs.find(t => t.value! > 0);
assert.ok(receiveTx, 'Should have at least one receive transaction');
assert.strictEqual(receiveTx.memo, 'test invoice');
assert.strictEqual(receiveTx.value, 9999);
assert.strictEqual(receiveTx.timestamp, 1761224952);
assert.strictEqual(receiveTx.ispaid, true);
assert.ok(receiveTx.payment_hash);
assert.ok(receiveTx.payment_request);
assert.strictEqual(receiveTx.payment_preimage, '7244f7e956a91171038ea935d56cdb758cc36c345f0aa92764bfed6fe6fc9b17');
assert.ok(receiveTx.value! > 0);
assert.ok(receiveTx.timestamp! > 0);
assert.ok(receiveTx.memo);
// Find the submarine swap (outgoing) transaction
const sendTx = txs.find(t => t.value! < 0);
assert.ok(sendTx, 'Should have at least one send transaction');
assert.strictEqual(sendTx.value, -8001);
assert.strictEqual(sendTx.timestamp, 1761225645);
assert.strictEqual(sendTx.ispaid, true);
assert.ok(sendTx.payment_hash);
assert.ok(sendTx.payment_request);
assert.strictEqual(sendTx.payment_preimage, '182fb8f273bda01b22c0e91991e093e18b2970f389fc7f7a2121870324eb2de5');
const swapHistory: any[] = (w as any)._swapHistory ?? [];
const settledReverse = swapHistory.find(s => s.type === 'reverse' && s.status === 'invoice.settled');
if (settledReverse) {
// When Boltz reverse-swap history is restored, settled receives are enriched in place.
assert.strictEqual(receiveTx.ispaid, true);
assert.ok(receiveTx.payment_hash);
assert.ok(receiveTx.payment_request);
assert.ok(receiveTx.payment_preimage);
assert.notStrictEqual(receiveTx.memo, 'Received');
const ownInvoice = settledReverse.request?.invoice || settledReverse.response?.invoice;
if (ownInvoice) {
assert.ok(w.isInvoiceGeneratedByWallet(ownInvoice));
}
}
const settledSubmarine = swapHistory.find(s => s.type === 'submarine' && s.status === 'transaction.claimed');
if (settledSubmarine) {
const sendTx = txs.find(t => t.value! < 0);
assert.ok(sendTx, 'Should have a send transaction when submarine swap history exists');
assert.strictEqual(sendTx.ispaid, true);
assert.ok(sendTx.payment_hash);
assert.ok(sendTx.payment_request);
assert.ok(sendTx.payment_preimage);
}
const invoices = await w.getUserInvoices();
assert.ok(invoices.length > 0);
assert(invoices[0].value! > 0);
assert(invoices[0].ispaid);
assert.ok(
w.isInvoiceGeneratedByWallet(
'lnbc100u1p50528cpp5rhy4fgs0ff23asecxtxt9zvc3apn0p8h7fxsj0d5k7j3x92zwhlqdq5w3jhxapqd9h8vmmfvdjscqrp80xqyf8ucsp5vcsrzye432n9wh0zwuv5z8y5n9zvkwpctr685e80utzc2yueccms9qxpqysgqd87swq3hput9k6llp0wxg098hc7ge3e5nrtnvak6zreywzaf4k9s8d3u4hrmt3m22kf0jt7ruqj0caknk5ykzdenjdphz50t7xrstnqqn6aw0m',
),
);
assert.ok(
!w.isInvoiceGeneratedByWallet(
'lnbc80u1p5052hwpp5z4ln6hyq4wcck809pt7f0q54ag5he6ce797flm7gl9vuccm9lx2sdqqcqzysxqyz5vqsp5nh9fl4g36606tvxswtnfxzy55yze2656cw2fya7dhl8r6u0czyds9qxpqysgq83sw25g9d9ltr05nkfzejnvvunzkrk4qeuxhszuvvsguk5m6vmg3a7n5nd67l9frru3kjzpt8x6jfusjyc7ezh49jeeh900kt3v30qsqzq7fst',
),
);
if (settledReverse) {
assert.ok(invoices.length > 0);
assert(invoices[0].value! > 0);
assert(invoices[0].ispaid);
}
});
// eslint-disable-next-line jest/no-disabled-tests

View File

@ -1,20 +0,0 @@
import assert from 'assert';
import { isGroundControlUriValid } from '../../blue_modules/notifications';
// Notifications.default = new Notifications();
describe('notifications', () => {
// yeah, lets rely less on external services...
// eslint-disable-next-line jest/no-disabled-tests
it.skip('can check groundcontrol server uri validity', async () => {
assert.ok(await isGroundControlUriValid('https://groundcontrol.bluewallet.io/'));
assert.ok(!(await isGroundControlUriValid('https://www.google.com')));
await new Promise(resolve => setTimeout(resolve, 2000));
});
// muted because it causes jest to hang waiting indefinitely
// eslint-disable-next-line jest/no-disabled-tests
it.skip('can check non-responding url', async () => {
assert.ok(!(await isGroundControlUriValid('https://localhost.com')));
});
});

View File

@ -7,7 +7,8 @@ console.warn = (...args) => {
if (
typeof args[0] === 'string' &&
(args[0].startsWith('WARNING: Sending to a future segwit version address can lead to loss of funds') ||
args[0].startsWith('only compressed public keys are good'))
args[0].startsWith('only compressed public keys are good') ||
args[0].startsWith('Using standard fetch instead of expo/fetch'))
) {
return;
}
@ -510,6 +511,17 @@ jest.mock('../blue_modules/analytics', () => {
return ret;
});
// addInvoice() registers a fire-and-forget payment-push callback; disable the
// URI in unit tests so node-fetch does not leave in-flight handles after the
// suite exits (which makes Jest fail with "did not exit one second after").
jest.mock('../blue_modules/constants', () => {
const actual = jest.requireActual('../blue_modules/constants');
return {
...actual,
arkadePaymentPushUri: '',
};
});
jest.mock('react-native-share', () => {
return {
open: jest.fn(),

View File

@ -1,8 +1,14 @@
import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import { _setSkipUpdateExchangeRate } from '../../blue_modules/currency';
import TransactionStatus from '../../screen/transactions/TransactionStatus';
// TransactionStatus renders fiat amounts via satoshiToLocalCurrency(), which
// kicks off a real exchange-rate fetch when no rate is cached — leaving a TLS
// socket open after the run ("Jest did not exit one second after...").
_setSkipUpdateExchangeRate();
type MockStorage = {
wallets: any[];
txMetadata: Record<string, any>;