diff --git a/src/lndmobile/index.ts b/src/lndmobile/index.ts index 7c0f9da26..5990f3c22 100644 --- a/src/lndmobile/index.ts +++ b/src/lndmobile/index.ts @@ -245,6 +245,7 @@ export const sendPaymentV2Sync = ( multiPath?: boolean, maxLNFeePercentage: number = 2, outgoingChanId?: Long, + isAmp?: boolean, ): Promise => { const maxFeeRatio = (maxLNFeePercentage ?? 2) / 100; @@ -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; diff --git a/src/state/LndMobileInjection.ts b/src/state/LndMobileInjection.ts index 67239e13f..f4c639a8c 100644 --- a/src/state/LndMobileInjection.ts +++ b/src/state/LndMobileInjection.ts @@ -131,6 +131,7 @@ export interface ILndMobileInjections { multiPath?: boolean, maxLNFeePercentage?: number, outgoingChannelId?: Long, + isAmp?: boolean, ) => Promise; queryRoutes: ( pubkey: string, diff --git a/src/state/Send.ts b/src/state/Send.ts index 891c319d5..0dd6ceec8 100644 --- a/src/state/Send.ts +++ b/src/state/Send.ts @@ -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"; @@ -32,6 +32,7 @@ export interface ISendModelSetPaymentPayload { export interface IModelSendPaymentPayload { amount?: Long; outgoingChannelId?: Long; + isAmpInvoice?: boolean; } export interface IModelQueryRoutesPayload { @@ -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, @@ -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: [], }; @@ -235,6 +245,7 @@ export const send: ISendModel = { multiPathPaymentsEnabled, maxLNFeePercentage, outgoingChannelId, + payload && payload.isAmpInvoice ? true : false, ); } catch (error) { await dispatch.transaction.syncTransaction({ @@ -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) => ({ diff --git a/src/state/Transaction.ts b/src/state/Transaction.ts index d0c9afd29..8f0f76a55 100644 --- a/src/state/Transaction.ts +++ b/src/state/Transaction.ts @@ -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"; @@ -26,8 +31,14 @@ export interface ITransactionModel { transactions: ITransaction[]; getTransactionByRHash: Computed ITransaction | undefined>; - getTransactionByPreimage: Computed ITransaction | undefined>; - getTransactionByPaymentRequest: Computed ITransaction | undefined>; + getTransactionByPreimage: Computed< + ITransactionModel, + (preimage: Uint8Array) => ITransaction | undefined + >; + getTransactionByPaymentRequest: Computed< + ITransactionModel, + (paymentRequest: string) => ITransaction | undefined + >; } export const transaction: ITransactionModel = { @@ -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; } } @@ -112,32 +136,37 @@ 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 = { @@ -145,7 +174,9 @@ export const transaction: ITransactionModel = { 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 = { @@ -153,12 +184,14 @@ export const transaction: ITransactionModel = { 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", @@ -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", @@ -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", @@ -192,7 +225,7 @@ export const transaction: ITransactionModel = { if (hideExpiredInvoices) { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); } - actions.updateTransaction({ transaction: updated }) + actions.updateTransaction({ transaction: updated }); }); } } @@ -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; + }); + }; + }), }; diff --git a/src/storage/database/transaction.ts b/src/storage/database/transaction.ts index 708265252..4cacf933c 100644 --- a/src/storage/database/transaction.ts +++ b/src/storage/database/transaction.ts @@ -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; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index bde2a7e10..49698bc97 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -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("💩", "@"); diff --git a/src/windows/LightningInfo/OpenChannel.tsx b/src/windows/LightningInfo/OpenChannel.tsx index 8f62883b9..e4beb11ac 100644 --- a/src/windows/LightningInfo/OpenChannel.tsx +++ b/src/windows/LightningInfo/OpenChannel.tsx @@ -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); diff --git a/src/windows/Overview.tsx b/src/windows/Overview.tsx index 597c65f06..3aec895e0 100644 --- a/src/windows/Overview.tsx +++ b/src/windows/Overview.tsx @@ -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"; @@ -191,7 +191,9 @@ function Overview({ navigation }: IOverviewProps) { } return ( navigation.navigate("TransactionDetails", { rHash })} diff --git a/src/windows/Send/SendConfirmation.tsx b/src/windows/Send/SendConfirmation.tsx index 18490c0bb..80add476c 100644 --- a/src/windows/Send/SendConfirmation.tsx +++ b/src/windows/Send/SendConfirmation.tsx @@ -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"; @@ -151,6 +151,8 @@ export default function SendConfirmation({ navigation, route }: ISendConfirmatio return {t("msg.error", { ns: namespaces.common })}; } + const isAmpInvoice = paymentRequest.features.hasOwnProperty(AMP_FEATURE_BIT); + const { name, description } = extractDescription(paymentRequest.description); const send = async () => { @@ -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);