Skip to content

Commit

Permalink
display human readable time until expiry
Browse files Browse the repository at this point in the history
  • Loading branch information
myxmaster committed Nov 15, 2023
1 parent 5d8643f commit 4e75cec
Show file tree
Hide file tree
Showing 12 changed files with 517 additions and 89 deletions.
2 changes: 1 addition & 1 deletion components/KeyValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface KeyValueProps {
sensitive?: boolean;
mempoolLink?: () => void;
disableCopy?: boolean;
SettingsStore: SettingsStore;
SettingsStore?: SettingsStore;
}

@inject('SettingsStore')
Expand Down
3 changes: 2 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,8 @@
"views.Invoice.receipt": "Receipt",
"views.Invoice.settleDate": "Settle Date",
"views.Invoice.creationDate": "Creation Date",
"views.Invoice.expiration": "Expiration",
"views.Invoice.originalExpiration": "Original Expiration",
"views.Invoice.expiration": "Time until Expiry",
"views.Invoice.private": "Private",
"views.Invoice.fallbackAddress": "Fallback Address",
"views.Invoice.cltvExpiry": "CLTV Expiry",
Expand Down
133 changes: 101 additions & 32 deletions models/Invoice.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { observable, computed } from 'mobx';
import BigNumber from 'bignumber.js';
import humanizeDuration from 'humanize-duration';

import BaseModel from './BaseModel';
import Base64Utils from './../utils/Base64Utils';
import DateTimeUtils from './../utils/DateTimeUtils';
import Bolt11Utils from './../utils/Bolt11Utils';
import { localeString } from './../utils/LocaleUtils';

interface HopHint {
Expand Down Expand Up @@ -72,6 +73,9 @@ export default class Invoice extends BaseModel {
public millisatoshis?: string;
public pay_req?: string;

public formattedOriginalTimeUntilExpiry: string;
public formattedTimeUntilExpiry: string;

@computed public get model(): string {
return localeString('views.Invoice.title');
}
Expand Down Expand Up @@ -233,41 +237,14 @@ export default class Invoice extends BaseModel {
return DateTimeUtils.listFormattedDate(this.creation_date);
}

@computed public get expirationDate(): Date | string {
const expiry = this.expiry || this.expire_time;

// handle LNDHub
if (expiry && new BigNumber(expiry).gte(1600000000)) {
return DateTimeUtils.listFormattedDate(expiry);
}

if (expiry) {
if (expiry == '0') return localeString('models.Invoice.never');
return `${expiry} ${localeString('models.Invoice.seconds')}`;
}

return this.expires_at
? DateTimeUtils.listFormattedDate(this.expires_at)
: localeString('models.Invoice.never');
}

@computed public get isExpired(): boolean {
const expiry = this.expiry || this.expire_time;

if (expiry && new BigNumber(expiry).gte(1600000000)) {
return (
new Date().getTime() > DateTimeUtils.listDate(expiry).getTime()
);
}
const getExpiryTimestamp = this.getExpiryUnixTimestamp();

if (expiry) {
return (
new Date().getTime() / 1000 >
Number(this.creation_date) + Number(expiry)
);
if (getExpiryTimestamp == null) {
return false;
}

return false;
return getExpiryTimestamp * 1000 <= Date.now();
}

@computed public get getKeysendMessage(): string {
Expand All @@ -288,4 +265,96 @@ export default class Invoice extends BaseModel {

return '';
}

@computed public get originalTimeUntilExpiryInSeconds():
| number
| undefined {
const decodedPaymentRequest = Bolt11Utils.decode(
this.getPaymentRequest
);
if (this.expires_at != null) {
// expiry is missing in payment request in Core Lightning
return this.expires_at - decodedPaymentRequest.timestamp;
}
return decodedPaymentRequest.expiry;
}

public determineFormattedOriginalTimeUntilExpiry(
locale: string | undefined
): void {
const originalTimeUntilExpiryInSeconds =
this.originalTimeUntilExpiryInSeconds;

if (originalTimeUntilExpiryInSeconds == null) {
return localeString('models.Invoice.never');
}

const originalTimeUntilExpiryInMs =
originalTimeUntilExpiryInSeconds * 1000;

this.formattedOriginalTimeUntilExpiry =
this.formatHumanReadableDuration(
originalTimeUntilExpiryInMs,
locale
);
}

public determineFormattedRemainingTimeUntilExpiry(
locale: string | undefined
): void {
const millisecondsUntilExpiry =
this.getRemainingMillisecondsUntilExpiry();

if (millisecondsUntilExpiry == null) {
this.formattedTimeUntilExpiry = localeString(
'models.Invoice.never'
);
return;
}

this.formattedTimeUntilExpiry =
millisecondsUntilExpiry <= 0
? localeString('views.Activity.expired')
: this.formatHumanReadableDuration(
millisecondsUntilExpiry,
locale
);
}

private getRemainingMillisecondsUntilExpiry(): number | undefined {
const expiryTimestamp = this.getExpiryUnixTimestamp();

return expiryTimestamp != null
? expiryTimestamp * 1000 - Date.now()
: undefined;
}

private getExpiryUnixTimestamp(): number | undefined {
const originalTimeUntilExpiryInSeconds =
this.originalTimeUntilExpiryInSeconds;

if (originalTimeUntilExpiryInSeconds == null) {
return undefined;
}

const paymentRequestTimestamp = Bolt11Utils.decode(
this.getPaymentRequest
).timestamp;

return paymentRequestTimestamp + originalTimeUntilExpiryInSeconds;
}

private formatHumanReadableDuration(
durationInMs: number,
locale: string | undefined
) {
return humanizeDuration(durationInMs, {
language: locale === 'zh' ? 'zh_CN' : locale,
fallbacks: ['en'],
round: true,
largest: 2
})
.replace(/(\d+) /g, '$1 ')
.replace(/ (\d+)/g, ' $1');
}
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@
"@remobile/react-native-qrcode-local-image": "github:BlueWallet/react-native-qrcode-local-image#31b0113",
"@tradle/react-native-http": "2.0.1",
"@types/dateformat": "5.0.0",
"@types/humanize-duration": "3.27.1",
"@types/react-native-snap-carousel": "3.8.5",
"assert": "1.5.0",
"base-x": "4.0.0",
"bc-ur": "0.1.6",
"bech32": "2.0.0",
"bignumber.js": "9.0.2",
"bip39": "3.1.0",
"bitcoinjs-lib": "3.3.2",
Expand All @@ -64,6 +66,7 @@
"fast-sha256": "1.3.0",
"hash.js": "1.1.7",
"https-browserify": "0.0.1",
"humanize-duration": "3.28.0",
"identicon.js": "2.3.3",
"inherits": "2.0.4",
"js-lnurl": "0.5.1",
Expand Down
24 changes: 16 additions & 8 deletions stores/ActivityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export default class ActivityStore {
};

getSortedActivity = () => {
const activity: any = [];
const activity: any[] = [];
const payments = this.paymentsStore.payments;
const transactions = this.transactionsStore.transactions;
const invoices = this.invoicesStore.invoices;
Expand Down Expand Up @@ -163,18 +163,18 @@ export default class ActivityStore {
};

@action
public updateInvoices = async () => {
public updateInvoices = async (locale: string | undefined) => {
await this.invoicesStore.getInvoices();
this.activity = this.getSortedActivity();
await this.setFilters(this.filters);
await this.setFilters(this.filters, locale);
};

@action
public updateTransactions = async () => {
public updateTransactions = async (locale: string | undefined) => {
if (BackendUtils.supportsOnchainSends())
await this.transactionsStore.getTransactions();
this.activity = this.getSortedActivity();
await this.setFilters(this.filters);
await this.setFilters(this.filters, locale);
};

@action
Expand All @@ -201,20 +201,28 @@ export default class ActivityStore {
}

@action
public setFilters = async (filters: Filter) => {
public setFilters = async (filters: Filter, locale: string | undefined) => {
this.loading = true;
this.filters = filters;
this.filteredActivity = ActivityFilterUtils.filterActivities(
this.activity,
filters
);
this.filteredActivity.forEach((activity) => {
if (activity instanceof Invoice) {
activity.determineFormattedRemainingTimeUntilExpiry(locale);
}
});
await EncryptedStorage.setItem(STORAGE_KEY, JSON.stringify(filters));
this.loading = false;
};

@action
public getActivityAndFilter = async (filters: Filter = this.filters) => {
public getActivityAndFilter = async (
locale: string | undefined,
filters: Filter = this.filters
) => {
await this.getActivity();
await this.setFilters(filters);
await this.setFilters(filters, locale);
};
}
12 changes: 12 additions & 0 deletions utils/Base64Utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,16 @@ describe('Base64Utils', () => {
);
});
});

describe('bytesToUtf8', () => {
it('converts a byte array to utf-8', () => {
const input = Uint8Array.from([
84, 104, 105, 115, 32, 105, 115, 32, 97, 32, 116, 101, 115, 116
]);

const utf8 = Base64Utils.bytesToUtf8(input);

expect(utf8).toBe('This is a test');
});
});
});
2 changes: 2 additions & 0 deletions utils/Base64Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class Base64Utils {
''
);

bytesToUtf8 = (input: Uint8Array) => Buffer.from(input).toString('utf-8');

utf8ToHex = (input: string) => Buffer.from(input, 'utf8').toString('hex');

base64UrlToHex = (input: string) =>
Expand Down
23 changes: 23 additions & 0 deletions utils/Bolt11Utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Bolt11Utils from './Bolt11Utils';

describe('decode', () => {
it('correctly decodes a valid payment request', () => {
const paymentRequest =
'lnbcrt1230n1pj429x7pp57t97q4awqj3f529snr0pa6senk83sq5pp760qf5a4jzvd7xgwcksdqqcqzzsxqrrsssp57eqtv7vxr46arupna3w4ct0lkf2mqmz9wt044cwkks0rwlnhfr5s9qyyssqragwpwav7nfwv2xyuuamxxj4pnnpzv2hlw7j473repd3sq7st698ta9kmzmygt0w7tmncl56a6mnma0w7e5dlpqd0wy6x3v35rssldspjhh8p0';

const decoded = Bolt11Utils.decode(paymentRequest);

expect(decoded.expiry).toBe(3600);
expect(decoded.timestamp).toBe(1700074718);
expect(decoded.paymentRequest).toBe(paymentRequest);
});

it('throws an error if an invalid payment request is given', () => {
const paymentRequest =
'bcrt1230n1pj429x7pp57t97q4awqj3f529snr0pa6senk83sq5pp760qf5a4jzvd7xgwcksdqqcqzzsxqrrsssp57eqtv7vxr46arupna3w4ct0lkf2mqmz9wt044cwkks0rwlnhfr5s9qyyssqragwpwav7nfwv2xyuuamxxj4pnnpzv2hlw7j473repd3sq7st698ta9kmzmygt0w7tmncl56a6mnma0w7e5dlpqd0wy6x3v35rssldspjhh8p0';

const action = () => Bolt11Utils.decode(paymentRequest);

expect(action).toThrowError('Not a proper lightning payment request');
});
});

0 comments on commit 4e75cec

Please sign in to comment.