Skip to content

Commit

Permalink
feat: deeplink-schema-match typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
limpbrains committed Feb 18, 2024
1 parent 9ac475c commit 1eb4833
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 65 deletions.
17 changes: 6 additions & 11 deletions blue_modules/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const _shareOpen = async (filePath: string) => {
* Writes a file to fs, and triggers an OS sharing dialog, so user can decide where to put this file (share to cloud
* or perhabs messaging app). Provided filename should be just a file name, NOT a path
*/
const writeFileAndExport = async function (filename: string, contents: string) {
export const writeFileAndExport = async function (filename: string, contents: string) {
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`;
await RNFS.writeFile(filePath, contents);
Expand Down Expand Up @@ -75,7 +75,7 @@ const writeFileAndExport = async function (filename: string, contents: string) {
/**
* Opens & reads *.psbt files, and returns base64 psbt. FALSE if something went wrong (wont throw).
*/
const openSignedTransaction = async function (): Promise<string | boolean> {
export const openSignedTransaction = async function (): Promise<string | boolean> {
try {
const res = await DocumentPicker.pickSingle({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles],
Expand Down Expand Up @@ -106,7 +106,7 @@ const _readPsbtFileIntoBase64 = async function (uri: string): Promise<string> {
}
};

const showImagePickerAndReadImage = () => {
export const showImagePickerAndReadImage = () => {
return new Promise((resolve, reject) =>
launchImageLibrary(
{
Expand Down Expand Up @@ -134,7 +134,7 @@ const showImagePickerAndReadImage = () => {
);
};

const showFilePickerAndReadFile = async function (): Promise<{ data: string | false; uri: string | false }> {
export const showFilePickerAndReadFile = async function (): Promise<{ data: string | false; uri: string | false }> {
try {
const res = await DocumentPicker.pickSingle({
copyTo: 'cachesDirectory',
Expand Down Expand Up @@ -194,18 +194,13 @@ const showFilePickerAndReadFile = async function (): Promise<{ data: string | fa
}
};

const readFileOutsideSandbox = (filePath: string) => {
export const readFileOutsideSandbox = (filePath: string) => {
if (Platform.OS === 'ios') {
return readFile(filePath);
} else if (Platform.OS === 'android') {
return RNFS.readFile(filePath);
} else {
presentAlert({ message: 'Not implemented for this platform' });
throw new Error('Not implemented for this platform');
}
};

module.exports.writeFileAndExport = writeFileAndExport;
module.exports.openSignedTransaction = openSignedTransaction;
module.exports.showFilePickerAndReadFile = showFilePickerAndReadFile;
module.exports.showImagePickerAndReadImage = showImagePickerAndReadImage;
module.exports.readFileOutsideSandbox = readFileOutsideSandbox;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NativeModules } from 'react-native';

const { BwFileAccess } = NativeModules;

export function readFile(filePath) {
export function readFile(filePath: string): Promise<string> {
return BwFileAccess.readFileContent(filePath);
}

Expand Down
4 changes: 2 additions & 2 deletions blue_modules/react-native-bw-file-access/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"title": "React Native Bw File Access",
"version": "1.0.0",
"description": "TODO",
"main": "index.js",
"main": "index.ts",
"homepage": "https://github.com/setavenger/react-native-bw-file-access",
"files": [
"README.md",
"android",
"index.js",
"index.ts",
"ios",
"react-native-bw-file-access.podspec"
],
Expand Down
11 changes: 8 additions & 3 deletions class/azteco.js → class/azteco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class Azteco {
*
* @returns {Promise<boolean>} Successfully redeemed or not. This method does not throw exceptions
*/
static async redeem(voucher, address) {
static async redeem(voucher: string, address: string): Promise<boolean> {
const api = new Frisbee({
baseURI: 'https://azte.co/',
});
Expand All @@ -24,14 +24,19 @@ export default class Azteco {
}
}

static isRedeemUrl(u) {
static isRedeemUrl(u: string): boolean {
return u.startsWith('https://azte.co');
}

static getParamsFromUrl(u) {
static getParamsFromUrl(u: string) {
const urlObject = URL.parse(u, true); // eslint-disable-line n/no-deprecated-api

if (urlObject.query.code) {
// check if code is a string
if (typeof urlObject.query.code !== 'string') {
throw new Error('Invalid URL');
}

// newer format of the url
return {
uri: u,
Expand Down
107 changes: 59 additions & 48 deletions class/deeplink-schema-match.js → class/deeplink-schema-match.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { LightningCustodianWallet, WatchOnlyWallet } from './';
import AsyncStorage from '@react-native-async-storage/async-storage';
import bip21, { TOptions } from 'bip21';
import * as bitcoin from 'bitcoinjs-lib';
import URL from 'url';

import { readFileOutsideSandbox } from '../blue_modules/fs';
import { Chain } from '../models/bitcoinUnits';
import Lnurl from './lnurl';
import { LightningCustodianWallet, WatchOnlyWallet } from './';
import Azteco from './azteco';
import { readFileOutsideSandbox } from '../blue_modules/fs';
import Lnurl from './lnurl';
import type { TWallet } from './wallets/types';

const bitcoin = require('bitcoinjs-lib');
const bip21 = require('bip21');
const BlueApp = require('../BlueApp');
const AppStorage = BlueApp.AppStorage;

type TCompletionHandlerParams = [string, object];
type TContext = {
wallets: TWallet[];
saveToDisk: () => void;
addWallet: (wallet: TWallet) => void;
setSharedCosigner: (cosigner: string) => void;
};

type TBothBitcoinAndLightning = { bitcoin: string; lndInvoice: string } | undefined;

class DeeplinkSchemaMatch {
static hasSchema(schemaString) {
static hasSchema(schemaString: string): boolean {
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false;
const lowercaseString = schemaString.trim().toLowerCase();
return (
Expand All @@ -33,9 +45,9 @@ class DeeplinkSchemaMatch {
* @param completionHandler {function} Callback that returns [string, params: object]
*/
static navigationRouteFor(
event,
completionHandler,
context = { wallets: [], saveToDisk: () => {}, addWallet: () => {}, setSharedCosigner: () => {} },
event: { url: string },
completionHandler: (args: TCompletionHandlerParams) => void,
context: TContext = { wallets: [], saveToDisk: () => {}, addWallet: () => {}, setSharedCosigner: () => {} },
) {
if (event.url === null) {
return;
Expand Down Expand Up @@ -121,7 +133,7 @@ class DeeplinkSchemaMatch {
})
.catch(e => console.warn(e));
}
let isBothBitcoinAndLightning;
let isBothBitcoinAndLightning: TBothBitcoinAndLightning;
try {
isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(event.url);
} catch (e) {
Expand All @@ -131,7 +143,7 @@ class DeeplinkSchemaMatch {
completionHandler([
'SelectWallet',
{
onWalletSelect: (wallet, { navigation }) => {
onWalletSelect: (wallet: TWallet, { navigation }: any) => {
navigation.pop(); // close select wallet screen
navigation.navigate(...DeeplinkSchemaMatch.isBothBitcoinAndLightningOnWalletSelect(wallet, isBothBitcoinAndLightning));
},
Expand Down Expand Up @@ -293,7 +305,7 @@ class DeeplinkSchemaMatch {
* @param url {string}
* @return {string|boolean}
*/
static getServerFromSetElectrumServerAction(url) {
static getServerFromSetElectrumServerAction(url: string): string | false {
if (!url.startsWith('bluewallet:setelectrumserver') && !url.startsWith('setelectrumserver')) return false;
const splt = url.split('server=');
if (splt[1]) return decodeURIComponent(splt[1]);
Expand All @@ -307,42 +319,42 @@ class DeeplinkSchemaMatch {
* @param url {string}
* @return {string|boolean}
*/
static getUrlFromSetLndhubUrlAction(url) {
static getUrlFromSetLndhubUrlAction(url: string): string | false {
if (!url.startsWith('bluewallet:setlndhuburl') && !url.startsWith('setlndhuburl')) return false;
const splt = url.split('url=');
if (splt[1]) return decodeURIComponent(splt[1]);
return false;
}

static isTXNFile(filePath) {
static isTXNFile(filePath: string): boolean {
return (
(filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) &&
filePath.toLowerCase().endsWith('.txn')
);
}

static isPossiblySignedPSBTFile(filePath) {
static isPossiblySignedPSBTFile(filePath: string): boolean {
return (
(filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) &&
filePath.toLowerCase().endsWith('-signed.psbt')
);
}

static isPossiblyPSBTFile(filePath) {
static isPossiblyPSBTFile(filePath: string): boolean {
return (
(filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) &&
filePath.toLowerCase().endsWith('.psbt')
);
}

static isPossiblyCosignerFile(filePath) {
static isPossiblyCosignerFile(filePath: string): boolean {
return (
(filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) &&
filePath.toLowerCase().endsWith('.bwcosigner')
);
}

static isBothBitcoinAndLightningOnWalletSelect(wallet, uri) {
static isBothBitcoinAndLightningOnWalletSelect(wallet: TWallet, uri: any): TCompletionHandlerParams {
if (wallet.chain === Chain.ONCHAIN) {
return [
'SendDetailsRoot',
Expand All @@ -354,7 +366,7 @@ class DeeplinkSchemaMatch {
},
},
];
} else if (wallet.chain === Chain.OFFCHAIN) {
} else {
return [
'ScanLndInvoiceRoot',
{
Expand All @@ -368,7 +380,7 @@ class DeeplinkSchemaMatch {
}
}

static isBitcoinAddress(address) {
static isBitcoinAddress(address: string): boolean {
address = address.replace('://', ':').replace('bitcoin:', '').replace('BITCOIN:', '').replace('bitcoin=', '').split('?')[0];
let isValidBitcoinAddress = false;
try {
Expand All @@ -380,7 +392,7 @@ class DeeplinkSchemaMatch {
return isValidBitcoinAddress;
}

static isLightningInvoice(invoice) {
static isLightningInvoice(invoice: string): boolean {
let isValidLightningInvoice = false;
if (
invoice.toLowerCase().startsWith('lightning:lnb') ||
Expand All @@ -392,15 +404,15 @@ class DeeplinkSchemaMatch {
return isValidLightningInvoice;
}

static isLnUrl(text) {
static isLnUrl(text: string): boolean {
return Lnurl.isLnurl(text);
}

static isWidgetAction(text) {
static isWidgetAction(text: string): boolean {
return text.startsWith('widget?action=');
}

static hasNeededJsonKeysForMultiSigSharing(str) {
static hasNeededJsonKeysForMultiSigSharing(str: string): boolean {
let obj;

// Check if it's a valid JSON
Expand All @@ -414,11 +426,11 @@ class DeeplinkSchemaMatch {
return typeof obj.xfp === 'string' && typeof obj.xpub === 'string' && typeof obj.path === 'string';
}

static isBothBitcoinAndLightning(url) {
static isBothBitcoinAndLightning(url: string): TBothBitcoinAndLightning {
if (url.includes('lightning') && (url.includes('bitcoin') || url.includes('BITCOIN'))) {
const txInfo = url.split(/(bitcoin:\/\/|BITCOIN:\/\/|bitcoin:|BITCOIN:|lightning:|lightning=|bitcoin=)+/);
let btc;
let lndInvoice;
let btc: string | false = false;
let lndInvoice: string | false = false;
for (const [index, value] of txInfo.entries()) {
try {
// Inside try-catch. We dont wan't to crash in case of an out-of-bounds error.
Expand Down Expand Up @@ -450,8 +462,10 @@ class DeeplinkSchemaMatch {
return undefined;
}

static bip21decode(uri) {
if (!uri) return {};
static bip21decode(uri?: string) {
if (!uri) {
throw new Error('No URI provided');
}
let replacedUri = uri;
for (const replaceMe of ['BITCOIN://', 'bitcoin://', 'BITCOIN:']) {
replacedUri = replacedUri.replace(replaceMe, 'bitcoin:');
Expand All @@ -460,37 +474,34 @@ class DeeplinkSchemaMatch {
return bip21.decode(replacedUri);
}

static bip21encode() {
const argumentsArray = Array.from(arguments);
for (const argument of argumentsArray) {
if (String(argument.label).replace(' ', '').length === 0) {
delete argument.label;
static bip21encode(address: string, options: TOptions): string {
for (const key in options) {
if (key === 'label' && String(options[key]).replace(' ', '').length === 0) {
delete options[key];
}
if (!(Number(argument.amount) > 0)) {
delete argument.amount;
if (key === 'amount' && !(Number(options[key]) > 0)) {
delete options[key];
}
}
return bip21.encode.apply(bip21, argumentsArray);
return bip21.encode(address, options);
}

static decodeBitcoinUri(uri) {
let amount = '';
let parsedBitcoinUri = null;
static decodeBitcoinUri(uri: string) {
let amount;
let address = uri || '';
let memo = '';
let payjoinUrl = '';
try {
parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri);
address = 'address' in parsedBitcoinUri ? parsedBitcoinUri.address : address;
const parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri);
address = parsedBitcoinUri.address ? parsedBitcoinUri.address.toString() : address;
if ('options' in parsedBitcoinUri) {
if ('amount' in parsedBitcoinUri.options) {
amount = parsedBitcoinUri.options.amount.toString();
amount = parsedBitcoinUri.options.amount;
if (parsedBitcoinUri.options.amount) {
amount = Number(parsedBitcoinUri.options.amount);
}
if ('label' in parsedBitcoinUri.options) {
memo = parsedBitcoinUri.options.label || memo;
if (parsedBitcoinUri.options.label) {
memo = parsedBitcoinUri.options.label;
}
if ('pj' in parsedBitcoinUri.options) {
if (parsedBitcoinUri.options.pj) {
payjoinUrl = parsedBitcoinUri.options.pj;
}
}
Expand Down
12 changes: 12 additions & 0 deletions typings/bip21.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
declare module 'bip21' {
export type TOptions =
| {
amount?: number;
label?: string;
pj?: string;
}
| { [key: string]: string };

export function decode(uri: string, urnScheme?: string): { address: string; options: TOptions };
export function encode(address: string, options?: TOptions, urnScheme?: string): string;
}

0 comments on commit 1eb4833

Please sign in to comment.