Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimization of opening payment channel when sending qi #406

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ POSTHOG_URL="https://app.posthog.com/capture/"
POSTHOG_API_KEY=
POSTHOG_PERSONAL_API_KEY=

MAILBOX_CONTRACT_ADDRESS="0x000432eE2e8444D3a6E29e5ecB72cb47418A5E2c"
MAILBOX_CONTRACT_ADDRESS="0x007889567f912CBE063224A4C81CBBC9Aec68a9c"
30 changes: 29 additions & 1 deletion background/redux-slices/qiSend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type QiSendState = {
receiverPaymentCode: string
amount: string
minerTip: string
channelExists: boolean
}

const initialState: QiSendState = {
Expand All @@ -19,6 +20,7 @@ const initialState: QiSendState = {
amount: "",
senderQuaiAccount: null,
minerTip: "",
channelExists: false,
}

const qiSendSlice = createSlice({
Expand Down Expand Up @@ -49,12 +51,16 @@ const qiSendSlice = createSlice({
) => {
immerState.senderQiAccount = payload
},
setQiChannelExists: (immerState, { payload }: { payload: boolean }) => {
immerState.channelExists = payload
},
resetQiSendSlice: (immerState) => {
immerState.senderQiAccount = null
immerState.senderQuaiAccount = null
immerState.amount = ""
immerState.receiverPaymentCode = ""
immerState.minerTip = ""
immerState.channelExists = false
},
},
})
Expand All @@ -65,6 +71,7 @@ export const {
setQiSendAmount,
setQiSendReceiverPaymentCode,
setQiSendMinerTip,
setQiChannelExists,
resetQiSendSlice,
} = qiSendSlice.actions

Expand All @@ -83,7 +90,7 @@ export const sendQiTransaction = createBackgroundAsyncThunk(
minerTip,
} = qiSend

const { address: quaiAddress } = senderQuaiAccount as AccountTotal
const { address: quaiAddress = "" } = senderQuaiAccount || {}
const { paymentCode: senderPaymentCode } =
senderQiAccount as UtxoAccountData

Expand All @@ -101,3 +108,24 @@ export const sendQiTransaction = createBackgroundAsyncThunk(
dispatch(resetQiSendSlice())
}
)

export const doesChannelExists = createBackgroundAsyncThunk(
"qiSend/checkPaymentChannel",
async (_, { getState, dispatch }) => {
const { qiSend } = getState() as RootState

const { senderQiAccount, receiverPaymentCode } = qiSend
const { paymentCode: senderPaymentCode } =
senderQiAccount as UtxoAccountData

const channelExists =
await main.transactionService.doesChannelExistForReceiver(
senderPaymentCode,
receiverPaymentCode
)

dispatch(setQiChannelExists(channelExists))

return channelExists
}
)
24 changes: 24 additions & 0 deletions background/services/transactions/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class TransactionsDatabase extends Dexie {

private qiTransactions!: Dexie.Table<QiTransactionDBEntry, [string, string]>

private openedPaymentChannels!: Dexie.Table<{ paymentCode: string }, number>

constructor(options?: DexieOptions) {
super("pelagus/transactions", options)
this.version(1).stores({
Expand All @@ -35,6 +37,10 @@ export class TransactionsDatabase extends Dexie {
qiTransactions:
"&[hash+chainId],hash,from,status,[from+chainId],to,[to+chainId],nonce,[nonce+from+chainId],blockHash,blockNumber,chainId,timestamp,firstSeen,dataSource",
})

this.version(3).stores({
openedPaymentChannels: "++id,paymentCode",
})
}

// ------------------------------------ quai tx ------------------------------------
Expand Down Expand Up @@ -170,6 +176,24 @@ export class TransactionsDatabase extends Dexie {
.firstSeen || Date.now()
)
}

// ------------------------------------- payment channels -------------------------------------
async addPaymentChannel(paymentCode: string): Promise<void> {
await this.openedPaymentChannels.add({ paymentCode })
}

async getPaymentChannel(
paymentCode: string
): Promise<{ paymentCode: string } | undefined> {
return this.openedPaymentChannels
.where("paymentCode")
.equals(paymentCode)
.first()
}

async getPaymentChannels(): Promise<{ paymentCode: string }[]> {
return this.openedPaymentChannels.toArray()
}
}

export function initializeTransactionsDatabase(
Expand Down
59 changes: 53 additions & 6 deletions background/services/transactions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,19 @@ export default class TransactionService extends BaseService<TransactionServiceEv
{}
)

// This should only be called if this is the first time the user
// has sent Qi to this payment code, otherwise, the transaction will fail
await this.notifyQiRecipient(
quaiAddress,
const channelExists = await this.doesChannelExistForReceiver(
senderPaymentCode,
receiverPaymentCode,
minerTip
receiverPaymentCode
)
if (!channelExists) {
await this.notifyQiRecipient(
quaiAddress,
senderPaymentCode,
receiverPaymentCode,
minerTip
)
}

NotificationsManager.createSendQiTxNotification()
} catch (error) {
logger.error("Failed to send Qi transaction", error)
Expand Down Expand Up @@ -344,6 +349,45 @@ export default class TransactionService extends BaseService<TransactionServiceEv
}
}

/**
* @returns {Promise<boolean>} - True if a channel is known and notification is unnecessary; false if receiver needs notification.
*/
public async doesChannelExistForReceiver(
senderPaymentCode: string,
receiverPaymentCode: string
): Promise<boolean> {
const { jsonRpcProvider } = this.chainService
const mailboxContract = new Contract(
this.MAILBOX_CONTRACT_ADDRESS,
MAILBOX_INTERFACE,
jsonRpcProvider
)

try {
// check if channel is established: receiver notified and local record exists
const [receiverPaymentChannels, paymentChannel] = await Promise.all([
mailboxContract.getNotifications(receiverPaymentCode),
this.db.getPaymentChannel(receiverPaymentCode),
])

if (receiverPaymentChannels.includes(senderPaymentCode)) {
if (paymentChannel) {
// channel is established and can be reopened using getNotifications on both sides
return true
}

// channel is established but only receiver knows about it, so we need update our local db
await this.db.addPaymentChannel(receiverPaymentCode)
return true
}
} catch (error) {
logger.error("Error checking if payment channel is established:", error)
throw error
}

return false
}

// ------------------------------------ private methods ------------------------------------
/**
* Fetches all transactions from the database and emits them to update the UI,
Expand Down Expand Up @@ -605,6 +649,9 @@ export default class TransactionService extends BaseService<TransactionServiceEv
gasOptions
)
await tx.wait()

// add payment channel if the recipient has been notified
await this.db.addPaymentChannel(receiverPaymentCode)
} catch (error) {
logger.error("Error occurs while notifying Qi recipient", error)
}
Expand Down
14 changes: 11 additions & 3 deletions ui/components/_NewDesign/ConfirmTransaction/ConfirmTransaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@ import FeeSettings from "./FeeSettings/FeeSettings"
import QuaiAccount from "./QuaiAccount/QuaiAccount"
import TransactionDetails from "./TransactionDetails/TransactionDetails"
import SharedErrorLabel from "../../Shared/_newDeisgn/errorLabel/SharedErrorLabel"
import { useBackgroundSelector } from "../../../hooks"

const ConfirmTransaction = ({
isInsufficientQuai,
}: {
isInsufficientQuai: boolean
}) => {
const { channelExists } = useBackgroundSelector((state) => state.qiSend)

return (
<>
<TransactionDetails />
<QuaiAccount />
{isInsufficientQuai && (
<SharedErrorLabel title="Insufficient funds to process transaction" />
{!channelExists && (
<>
<QuaiAccount />
{isInsufficientQuai && (
<SharedErrorLabel title="Insufficient funds to process transaction" />
)}
</>
)}

<FeeSettings />
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useState } from "react"
import { setQiSendMinerTip } from "@pelagus/pelagus-background/redux-slices/qiSend"
import { useBackgroundDispatch } from "../../../../hooks"
import { useBackgroundDispatch, useBackgroundSelector } from "../../../../hooks"

const FeeSettings = () => {
const dispatch = useBackgroundDispatch()
const { channelExists } = useBackgroundSelector((state) => state.qiSend)

const [isShowAdvancedSettings, setIsShowAdvancedSettings] = useState(false)

Expand All @@ -28,17 +29,19 @@ const FeeSettings = () => {
<>
<div>
<div className="fees">
<div className="fee-row">
<p className="fee-row-key">Payment Channel Gas Fee</p>
<p className="fee-row-value">- QUAI</p>
</div>
{!channelExists && (
<div className="fee-row">
<p className="fee-row-key">Payment Channel Gas Fee</p>
<p className="fee-row-value">- QUAI</p>
</div>
)}
<div className="fee-row">
<p className="fee-row-key">Estimated Fee</p>
<p className="fee-row-value">- QI</p>
</div>
</div>

{!isShowAdvancedSettings && (
{!channelExists && !isShowAdvancedSettings && (
<button
type="button"
className="advanced-settings"
Expand Down
14 changes: 10 additions & 4 deletions ui/pages/_NewDesign/ConfirmTransactionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,29 @@ const ConfirmTransactionPage = () => {
keyPrefix: "drawers.transactionConfirmation",
})

const { senderQuaiAccount } = useBackgroundSelector((state) => state.qiSend)
const { senderQuaiAccount, channelExists } = useBackgroundSelector(
(state) => state.qiSend
)
const { balance: quaiBalance = "" } = senderQuaiAccount ?? {}

const [isInsufficientQuai, setInsufficientQuai] = useState(false)
const [isOpenConfirmationModal, setIsOpenConfirmationModal] = useState(false)
const [isTransactionError, setIsTransactionError] = useState(false)

useEffect(() => {
if (channelExists) return

const serializedBalance = Number(quaiBalance.split(" ")[0])

if (senderQuaiAccount && !serializedBalance) {
setInsufficientQuai(true)
return
}
setInsufficientQuai(false)
}, [quaiBalance, senderQuaiAccount])
}, [quaiBalance, senderQuaiAccount, channelExists])

const onSendQiTransaction = async () => {
if (isInsufficientQuai) return
if (!channelExists && isInsufficientQuai) return

dispatch(sendQiTransaction())
setIsOpenConfirmationModal(true)
Expand Down Expand Up @@ -73,7 +77,9 @@ const ConfirmTransactionPage = () => {
<ConfirmTransaction isInsufficientQuai={isInsufficientQuai} />
<SharedActionButtons
title={{ confirmTitle: "Send", cancelTitle: "Back" }}
isConfirmDisabled={!senderQuaiAccount || isInsufficientQuai}
isConfirmDisabled={
!channelExists && (!senderQuaiAccount || isInsufficientQuai)
}
onClick={{
onConfirm: onSendQiTransaction,
onCancel: () => history.push("-1"),
Expand Down
17 changes: 14 additions & 3 deletions ui/pages/_NewDesign/SendPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { useHistory } from "react-router-dom"
import { selectShowPaymentChannelModal } from "@pelagus/pelagus-background/redux-slices/ui"
import { parseQi, Zone } from "quais"
import { selectCurrentNetwork } from "@pelagus/pelagus-background/redux-slices/selectors"
import { doesChannelExists } from "@pelagus/pelagus-background/redux-slices/qiSend"
import { AsyncThunkFulfillmentType } from "@pelagus/pelagus-background/redux-slices/utils"
import SendAsset from "../../components/_NewDesign/SendAsset/SendAsset"
import SharedGoBackPageHeader from "../../components/Shared/_newDeisgn/pageHeaders/SharedGoBackPageHeader"
import PaymentChanelModal from "../../components/_NewDesign/SendAsset/PaymentChanelModal/PaymentChanelModal"
import SharedActionButtons from "../../components/Shared/_newDeisgn/actionButtons/SharedActionButtons"
import { useBackgroundSelector } from "../../hooks"
import { useBackgroundDispatch, useBackgroundSelector } from "../../hooks"

const SendPage = () => {
const history = useHistory()
const dispatch = useBackgroundDispatch()

const currentNetwork = useBackgroundSelector(selectCurrentNetwork)
const utxoAccountsByPaymentCode = useBackgroundSelector(
Expand All @@ -24,6 +27,7 @@ const SendPage = () => {
)
const [isOpenPaymentChanelModal, setIsOpenPaymentChanelModal] =
useState(false)
const [isConfirmLoading, setIsConfirmLoading] = useState(false)

const { amount, receiverPaymentCode } = useBackgroundSelector(
(state) => state.qiSend
Expand All @@ -47,8 +51,14 @@ const SendPage = () => {
setIsConfirmDisabled(true)
}, [amount, receiverPaymentCode, utxoAccountArr])

const handleConfirm = () => {
if (!showPaymentChannelModal) {
const handleConfirm = async () => {
setIsConfirmLoading(true)
const channelExists = (await dispatch(
doesChannelExists()
)) as AsyncThunkFulfillmentType<typeof doesChannelExists>
setIsConfirmLoading(false)

if (channelExists || !showPaymentChannelModal) {
history.push("/send-qi/confirmation")
} else {
setIsOpenPaymentChanelModal(true)
Expand All @@ -67,6 +77,7 @@ const SendPage = () => {
onConfirm: () => handleConfirm(),
onCancel: () => history.push("/"),
}}
isLoading={isConfirmLoading}
/>
</main>
{isOpenPaymentChanelModal && (
Expand Down