Skip to content

Commit

Permalink
add support to pay to amp invoices
Browse files Browse the repository at this point in the history
Signed-off-by: Nitesh Balusu <84944042+niteshbalusu11@users.noreply.github.com>
  • Loading branch information
niteshbalusu11 committed Dec 11, 2023
1 parent ce1b7ce commit e442215
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 59 deletions.
2 changes: 2 additions & 0 deletions src/lndmobile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export const sendPaymentV2Sync = (
multiPath?: boolean,
maxLNFeePercentage: number = 2,
outgoingChanId?: Long,
isAmp?: boolean,
): Promise<lnrpc.Payment> => {
const maxFeeRatio = (maxLNFeePercentage ?? 2) / 100;

Expand All @@ -256,6 +257,7 @@ export const sendPaymentV2Sync = (
feeLimitSat: Long.fromValue(Math.max(10, (payAmount?.toNumber() || 0) * maxFeeRatio)),
cltvLimit: 0,
outgoingChanId,
amp: isAmp,
};
if (amount) {
options.amt = amount;
Expand Down
1 change: 1 addition & 0 deletions src/state/LndMobileInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export interface ILndMobileInjections {
multiPath?: boolean,
maxLNFeePercentage?: number,
outgoingChannelId?: Long,
isAmp?: boolean,
) => Promise<lnrpc.Payment>;
queryRoutes: (
pubkey: string,
Expand Down
16 changes: 14 additions & 2 deletions src/state/Send.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Bech32 from "bech32";

import { Action, Thunk, action, thunk } from "easy-peasy";
import { getGeolocation, hexToUint8Array } from "../utils";
import { getGeolocation, hexToUint8Array, uint8ArrayToString } from "../utils";
import { lnrpc, routerrpc } from "../../proto/lightning";

import { ILNUrlPayResponse } from "./LNURL";
Expand Down Expand Up @@ -32,6 +32,7 @@ export interface ISendModelSetPaymentPayload {
export interface IModelSendPaymentPayload {
amount?: Long;
outgoingChannelId?: Long;
isAmpInvoice?: boolean;
}

export interface IModelQueryRoutesPayload {
Expand Down Expand Up @@ -180,8 +181,15 @@ export const send: ISendModel = {
const getTransactionByPaymentRequest =
getStoreState().transaction.getTransactionByPaymentRequest;

const isAmpInvoice = payload && payload.isAmpInvoice ? true : false;

// getTransactionByPaymentRequest only if isAmpInvoice is false
const transactionByPaymentRequest = !isAmpInvoice
? getTransactionByPaymentRequest(paymentRequestStr)
: undefined;

// Pre-settlement tx insert
const preTransaction: ITransaction = getTransactionByPaymentRequest(paymentRequestStr) ?? {
const preTransaction: ITransaction = transactionByPaymentRequest ?? {
date: paymentRequest.timestamp,
description: extraData.lnurlPayTextPlain ?? paymentRequest.description,
duration: 0,
Expand Down Expand Up @@ -212,12 +220,14 @@ export const send: ISendModel = {
paymentRequest.description,
extraData.website,
),
ampInvoice: isAmpInvoice,
//note: // TODO: Why wasn't this added
lightningAddress: extraData.lightningAddress ?? null,
lud16IdentifierMimeType: extraData.lud16IdentifierMimeType ?? null,

preimage: hexToUint8Array("0"),
lnurlPayResponse: extraData.lnurlPayResponse,
lud18PayerData: null,

hops: [],
};
Expand All @@ -235,6 +245,7 @@ export const send: ISendModel = {
multiPathPaymentsEnabled,
maxLNFeePercentage,
outgoingChannelId,
payload && payload.isAmpInvoice ? true : false,
);
} catch (error) {
await dispatch.transaction.syncTransaction({
Expand Down Expand Up @@ -263,6 +274,7 @@ export const send: ISendModel = {
feeMsat: sendPaymentResult.feeMsat || Long.fromInt(0),

preimage: hexToUint8Array(sendPaymentResult.paymentPreimage),
ampInvoice: isAmpInvoice,

hops:
sendPaymentResult.htlcs[0].route?.hops?.map((hop) => ({
Expand Down
135 changes: 83 additions & 52 deletions src/state/Transaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { LayoutAnimation } from "react-native";
import { Thunk, thunk, Action, action, Computed, computed } from "easy-peasy";
import { ITransaction, getTransactions, createTransaction, updateTransaction } from "../storage/database/transaction";
import {
ITransaction,
getTransactions,
createTransaction,
updateTransaction,
} from "../storage/database/transaction";

import { IStoreModel } from "./index";
import { IStoreInjections } from "./store";
Expand All @@ -26,8 +31,14 @@ export interface ITransactionModel {

transactions: ITransaction[];
getTransactionByRHash: Computed<ITransactionModel, (rHash: string) => ITransaction | undefined>;
getTransactionByPreimage: Computed<ITransactionModel, (preimage: Uint8Array) => ITransaction | undefined>;
getTransactionByPaymentRequest: Computed<ITransactionModel, (paymentRequest: string) => ITransaction | undefined>;
getTransactionByPreimage: Computed<
ITransactionModel,
(preimage: Uint8Array) => ITransaction | undefined
>;
getTransactionByPaymentRequest: Computed<
ITransactionModel,
(paymentRequest: string) => ITransaction | undefined
>;
}

export const transaction: ITransactionModel = {
Expand All @@ -42,13 +53,26 @@ export const transaction: ITransactionModel = {
throw new Error("syncTransaction(): db not ready");
}

// Don't insert open transactions for AMP invoices
if (tx.status === "OPEN" && tx.ampInvoice) {
return;
}

// If AMP invoice settles, insert a new tx
if (tx.status === "SETTLED" && tx.ampInvoice) {
const id = await createTransaction(db, tx);
actions.addTransaction({ ...tx, id });

return;
}

const transactions = getState().transactions;
let foundTransaction = false;

for (const txIt of transactions) {
if (txIt.paymentRequest === tx.paymentRequest) {
await updateTransaction(db, { ...txIt, ...tx });
actions.updateTransaction({ transaction: { ...txIt, ...tx }});
actions.updateTransaction({ transaction: { ...txIt, ...tx } });
foundTransaction = true;
}
}
Expand Down Expand Up @@ -112,53 +136,62 @@ export const transaction: ITransactionModel = {
throw new Error("checkOpenTransactions(): db not ready");
}


for (const tx of getState().transactions) {
if (tx.status === "OPEN") {
log.i("trackpayment tx", [tx.rHash]);
if (tx.valueMsat.isNegative()) {
trackPayment(tx.rHash).then((trackPaymentResult) => {
log.i("trackpayment status", [trackPaymentResult.status, trackPaymentResult.paymentHash]);
log.i("trackpayment status", [
trackPaymentResult.status,
trackPaymentResult.paymentHash,
]);
if (trackPaymentResult.status === lnrpc.Payment.PaymentStatus.SUCCEEDED) {
log.i("trackpayment updating tx [settled]");
const updated: ITransaction = {
...tx,
status: "SETTLED",
preimage: hexToUint8Array(trackPaymentResult.paymentPreimage),
hops: trackPaymentResult.htlcs[0].route?.hops?.map((hop) => ({
chanId: hop.chanId ?? null,
chanCapacity: hop.chanCapacity ?? null,
amtToForward: hop.amtToForward || Long.fromInt(0),
amtToForwardMsat: hop.amtToForwardMsat || Long.fromInt(0),
fee: hop.fee || Long.fromInt(0),
feeMsat: hop.feeMsat || Long.fromInt(0),
expiry: hop.expiry || null,
pubKey: hop.pubKey || null,
})) ?? [],
hops:
trackPaymentResult.htlcs[0].route?.hops?.map((hop) => ({
chanId: hop.chanId ?? null,
chanCapacity: hop.chanCapacity ?? null,
amtToForward: hop.amtToForward || Long.fromInt(0),
amtToForwardMsat: hop.amtToForwardMsat || Long.fromInt(0),
fee: hop.fee || Long.fromInt(0),
feeMsat: hop.feeMsat || Long.fromInt(0),
expiry: hop.expiry || null,
pubKey: hop.pubKey || null,
})) ?? [],
};
// tslint:disable-next-line
updateTransaction(db, updated).then(() => actions.updateTransaction({ transaction: updated }));
updateTransaction(db, updated).then(() =>
actions.updateTransaction({ transaction: updated }),
);
} else if (trackPaymentResult.status === lnrpc.Payment.PaymentStatus.UNKNOWN) {
log.i("trackpayment updating tx [unknown]");
const updated: ITransaction = {
...tx,
status: "UNKNOWN",
};
// tslint:disable-next-line
updateTransaction(db, updated).then(() => actions.updateTransaction({ transaction: updated }));
updateTransaction(db, updated).then(() =>
actions.updateTransaction({ transaction: updated }),
);
} else if (trackPaymentResult.status === lnrpc.Payment.PaymentStatus.FAILED) {
log.i("trackpayment updating tx [failed]");
const updated: ITransaction = {
...tx,
status: "CANCELED",
};
// tslint:disable-next-line
updateTransaction(db, updated).then(() => actions.updateTransaction({ transaction: updated }));
updateTransaction(db, updated).then(() =>
actions.updateTransaction({ transaction: updated }),
);
}
});
} else {
const check = await lookupInvoice(tx.rHash);
if ((Date.now() / 1000) > (check.creationDate.add(check.expiry).toNumber())) {
if (Date.now() / 1000 > check.creationDate.add(check.expiry).toNumber()) {
const updated: ITransaction = {
...tx,
status: "EXPIRED",
Expand All @@ -170,8 +203,7 @@ export const transaction: ITransactionModel = {
}
actions.updateTransaction({ transaction: updated });
});
}
else if (check.settled) {
} else if (check.settled) {
const updated: ITransaction = {
...tx,
status: "SETTLED",
Expand All @@ -180,9 +212,10 @@ export const transaction: ITransactionModel = {
// TODO add valueUSD, valueFiat and valueFiatCurrency?
};
// tslint:disable-next-line
updateTransaction(db, updated).then(() => actions.updateTransaction({ transaction: updated }));
}
else if (check.state === lnrpc.Invoice.InvoiceState.CANCELED) {
updateTransaction(db, updated).then(() =>
actions.updateTransaction({ transaction: updated }),
);
} else if (check.state === lnrpc.Invoice.InvoiceState.CANCELED) {
const updated: ITransaction = {
...tx,
status: "CANCELED",
Expand All @@ -192,7 +225,7 @@ export const transaction: ITransactionModel = {
if (hideExpiredInvoices) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
}
actions.updateTransaction({ transaction: updated })
actions.updateTransaction({ transaction: updated });
});
}
}
Expand All @@ -204,32 +237,30 @@ export const transaction: ITransactionModel = {
/**
* Set transactions to our transaction array
*/
setTransactions: action((state, transactions) => { state.transactions = transactions; }),
setTransactions: action((state, transactions) => {
state.transactions = transactions;
}),

transactions: [],
getTransactionByRHash: computed(
(state) => {
return (rHash: string) => {
return state.transactions.find((tx) => rHash === tx.rHash);
};
},
),

getTransactionByPreimage: computed(
(state) => {
return (preimage: Uint8Array) => {
return state.transactions.find((tx) => bytesToHexString(preimage) === bytesToHexString(tx.preimage));
};
},
),

getTransactionByPaymentRequest: computed(
(state) => {
return (paymentRequest: string) => {
return state.transactions.find((tx) => {
return paymentRequest === tx.paymentRequest;
});
};
},
),
getTransactionByRHash: computed((state) => {
return (rHash: string) => {
return state.transactions.find((tx) => rHash === tx.rHash);
};
}),

getTransactionByPreimage: computed((state) => {
return (preimage: Uint8Array) => {
return state.transactions.find(
(tx) => bytesToHexString(preimage) === bytesToHexString(tx.preimage),
);
};
}),

getTransactionByPaymentRequest: computed((state) => {
return (paymentRequest: string) => {
return state.transactions.find((tx) => {
return paymentRequest === tx.paymentRequest;
});
};
}),
};
1 change: 1 addition & 0 deletions src/storage/database/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface ITransaction {
paymentRequest: string;
status: "ACCEPTED" | "CANCELED" | "OPEN" | "SETTLED" | "UNKNOWN" | "EXPIRED"; // Note: EXPIRED does not exist in lnd
rHash: string;
ampInvoice: boolean;
nodeAliasCached: string | null;
payer?: string | null;
valueUSD: number | null;
Expand Down
1 change: 1 addition & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const TLV_RECORD_NAME = 128101;
export const TLV_KEYSEND = 5482373484;
export const TLV_WHATSAT_MESSAGE = 34349334;
export const TLV_SATOGRAM = 6789998212;
export const AMP_FEATURE_BIT = "30";

export const GITHUB_REPO_URL = "https://github.com/hsjoberg/blixt-wallet";
export const HAMPUS_EMAIL = "mailto:hampus.sjoberg💩protonmail.com".replace("💩", "@");
Expand Down
4 changes: 2 additions & 2 deletions src/windows/LightningInfo/OpenChannel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ export default function OpenChannel({ navigation, route }: IOpenChannelProps) {
await connectAndOpenChannelAll({
peer,
feeRateSat: feeRate !== 0 ? feeRate : undefined,
type: taprootChan ? lnrpc.CommitmentType["SIMPLE_TAPROOT"] : lnrpc.CommitmentType.ANCHORS,
type: taprootChan ? lnrpc.CommitmentType["SIMPLE_TAPROOT"] : undefined,
});
} else {
await connectAndOpenChannel({
peer,
amount: satoshiValue,
feeRateSat: feeRate !== 0 ? feeRate : undefined,
type: taprootChan ? lnrpc.CommitmentType["SIMPLE_TAPROOT"] : lnrpc.CommitmentType.ANCHORS,
type: taprootChan ? lnrpc.CommitmentType["SIMPLE_TAPROOT"] : undefined,
});
}
await getChannels(undefined);
Expand Down
6 changes: 4 additions & 2 deletions src/windows/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { RootStackParamList } from "../Main";
import { useStoreActions, useStoreState } from "../state/store";
import TransactionCard from "../components/TransactionCard";
import Container from "../components/Container";
import { timeout, toast } from "../utils/index";
import { bytesToHexString, timeout, toast } from "../utils/index";
import { formatBitcoin, convertBitcoinToFiat } from "../utils/bitcoin-units";
import FooterNav from "../components/FooterNav";
import Drawer from "../components/Drawer";
Expand Down Expand Up @@ -191,7 +191,9 @@ function Overview({ navigation }: IOverviewProps) {
}
return (
<TransactionCard
key={transaction.rHash}
key={
transaction.ampInvoice ? bytesToHexString(transaction.preimage) : transaction.rHash
}
transaction={transaction}
unit={bitcoinUnit}
onPress={(rHash) => navigation.navigate("TransactionDetails", { rHash })}
Expand Down
5 changes: 4 additions & 1 deletion src/windows/Send/SendConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import BlixtForm, { IFormItem } from "../../components/Form";
import { hexToUint8Array, toast } from "../../utils";
import { useStoreActions, useStoreState } from "../../state/store";
import Input from "../../components/Input";
import { PLATFORM } from "../../utils/constants";
import { AMP_FEATURE_BIT, PLATFORM } from "../../utils/constants";
import { RouteProp } from "@react-navigation/native";
import { SendStackParamList } from "./index";
import { StackNavigationProp } from "@react-navigation/stack";
Expand Down Expand Up @@ -151,6 +151,8 @@ export default function SendConfirmation({ navigation, route }: ISendConfirmatio
return <Text>{t("msg.error", { ns: namespaces.common })}</Text>;
}

const isAmpInvoice = paymentRequest.features.hasOwnProperty(AMP_FEATURE_BIT);

const { name, description } = extractDescription(paymentRequest.description);

const send = async () => {
Expand All @@ -164,6 +166,7 @@ export default function SendConfirmation({ navigation, route }: ISendConfirmatio
: undefined,

outgoingChannelId: outChannel !== "any" ? Long.fromString(outChannel) : undefined,
isAmpInvoice,
};

const response = await sendPayment(payload);
Expand Down

0 comments on commit e442215

Please sign in to comment.