Skip to content
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
38 changes: 9 additions & 29 deletions paybutton/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -748,29 +748,8 @@
react-is "^19.1.1"

"@paybutton/react@link:../react":
version "5.2.1"
dependencies:
"@emotion/react" "^11.14.0"
"@emotion/styled" "^11.14.1"
"@mui/material" "^7.3.4"
"@types/crypto-js" "^4.2.1"
"@types/jest" "^29.5.11"
axios "1.12.0"
bignumber.js "9.0.2"
cashtab-connect "^1.1.0"
chronik-client-cashtokens "^3.4.0"
copy-to-clipboard "3.3.3"
crypto-js "^4.2.0"
decimal.js "^10.6.0"
ecashaddrjs "^2.0.0"
jest "^29.7.0"
lodash "4.17.21"
notistack "3.0.0"
qrcode.react "3"
react-number-format "^5.4.4"
socket.io-client "4.7.4"
ts-jest "^29.4.5"
xecaddrjs "^0.0.1"
version "0.0.0"
uid ""

"@popperjs/core@^2.11.8":
version "2.11.8"
Expand Down Expand Up @@ -1755,15 +1734,16 @@ chrome-trace-event@^1.0.2:
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b"
integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==

chronik-client-cashtokens@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/chronik-client-cashtokens/-/chronik-client-cashtokens-3.4.0.tgz#0c958c72c119867d924c0c446e0765381fd1b792"
integrity sha512-D2G8j+dhOlG0wr+DtgnMHHwCYzpa/XCzNvHhkkAfOOL3OaAoXONMxhbqx+N1y7B0L5ot69y8z/8KOEz9ARL3hg==
chronik-client@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chronik-client/-/chronik-client-4.1.0.tgz#f49a07565064335f1d576230bf77546a9bc29e8b"
integrity sha512-RA7gXMTmWW3ek29ACvHGfcJxGUBMxtcvMCCV7EJLywYCCYyCDmjdcTVIHEqEFF0QHj6F5PJi5nvA65cAtBnr5Q==
dependencies:
"@types/ws" "^8.2.1"
axios "^1.6.3"
ecashaddrjs "2.0.0"
ecashaddrjs "^2.0.0"
isomorphic-ws "^4.0.1"
long "^4.0.0"
protobufjs "^6.8.8"
ws "^8.3.0"

Expand Down Expand Up @@ -2006,7 +1986,7 @@ dunder-proto@^1.0.1:
es-errors "^1.3.0"
gopd "^1.2.0"

ecashaddrjs@2.0.0, ecashaddrjs@^1.0.7, ecashaddrjs@^2.0.0:
ecashaddrjs@^1.0.7, ecashaddrjs@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-2.0.0.tgz#d45ede7fb6168815dbcf664b8e0a6872e485d874"
integrity sha512-EvK1V4D3+nIEoD0ggy/b0F4lW39/72R9aOs/scm6kxMVuXu16btc+H74eQv7okNfXaQWKgolEekZkQ6wfcMMLw==
Expand Down
52 changes: 50 additions & 2 deletions react/lib/components/Widget/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
MINIMUM_ALTPAYMENT_DOLLAR_AMOUNT,
MINIMUM_ALTPAYMENT_CAD_AMOUNT,
} from '../../altpayment'
import { WsEndpoint } from 'chronik-client'


export interface WidgetProps {
Expand Down Expand Up @@ -221,10 +222,15 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {


// websockets if standalone
const [internalTxsSocket, setInternalTxsSocket] = useState<Socket | undefined>(undefined)
const [internalTxsSocket, setInternalTxsSocket] = useState<Socket | WsEndpoint | undefined>(undefined)
const thisTxsSocket = txsSocket ?? internalTxsSocket
const setThisTxsSocket =
(setTxsSocket as ((s: Socket | undefined) => void) | undefined) ?? setInternalTxsSocket
(setTxsSocket as ((s: Socket | WsEndpoint | undefined) => void) | undefined) ?? setInternalTxsSocket

// Type guard to check if socket is a Chronik WsEndpoint
const isChronikWsEndpoint = (socket: Socket | WsEndpoint | undefined): socket is WsEndpoint => {
return socket instanceof WsEndpoint;
};

const [internalNewTxs, setInternalNewTxs] = useState<Transaction[] | undefined>()
const thisNewTxs = newTxs ?? internalNewTxs
Expand Down Expand Up @@ -593,6 +599,48 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
}
}, [to, thisUseAltpayment])

// Suspend/resume Chronik WebSocket based on page visibility
useEffect(() => {
if (typeof document === 'undefined' || isChild === true) {
return;
}

const handleVisibilityChange = async () => {
if (!thisTxsSocket) {
// Not initialized yet
return;
}

// Check if this is a Chronik WsEndpoint (has pause/resume methods)
// vs a socket.io Socket (doesn't have these methods)
if (!isChronikWsEndpoint(thisTxsSocket)) {
return;
}

if (document.hidden) {
// Page went to background - suspend WebSocket
try {
thisTxsSocket.pause();
} catch (error) {
console.error('Error pausing WebSocket:', error);
}
} else {
// Page came to foreground - resume WebSocket
try {
await thisTxsSocket.resume();
} catch (error) {
console.error('Error resuming WebSocket:', error);
}
}
};

document.addEventListener('visibilitychange', handleVisibilityChange);

return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [isChild, thisTxsSocket])

const tradeWithAltpayment = () => {
setThisUseAltpayment(true)
}
Expand Down
56 changes: 24 additions & 32 deletions react/lib/components/Widget/WidgetContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
DEFAULT_DONATION_RATE,
POLL_TX_HISTORY_LOOKBACK,
POLL_REQUEST_DELAY,
POLL_MAX_RETRY,
} from '../../util';
import { getAddressDetails } from '../../util/api-client';

Expand Down Expand Up @@ -161,7 +160,6 @@ export const WidgetContainer: React.FunctionComponent<WidgetContainerProps> =
const [thisPrice, setThisPrice] = useState(0);
const [usdPrice, setUsdPrice] = useState(0);
const [success, setSuccess] = useState(false);
const [retryCount, setRetryCount] = useState(0);
const { enqueueSnackbar } = useSnackbar();

const [shiftCompleted, setShiftCompleted] = useState(false);
Expand Down Expand Up @@ -319,8 +317,15 @@ export const WidgetContainer: React.FunctionComponent<WidgetContainerProps> =

let wasHidden = document.hidden;
let hiddenTimestamp = 0;
let retryTimeoutId: NodeJS.Timeout | null = null;

const handleVisibilityChange = async () => {
// Clear any pending retry timeout
if (retryTimeoutId) {
clearTimeout(retryTimeoutId);
retryTimeoutId = null;
}

if (document.hidden) {
wasHidden = true;
hiddenTimestamp = Date.now();
Expand Down Expand Up @@ -352,46 +357,33 @@ export const WidgetContainer: React.FunctionComponent<WidgetContainerProps> =
const checkCompleted = await checkForTransactions();

// If check completed successfully but payment hasn't succeeded yet,
// trigger retries. We might be missing the payment transaction.
// Schedule a single retry after 2 seconds. This is only there to handle
// the case where the transaction is discovered after the app has been
// foregrounded, but before the chronik websocket is resumed. 2 seconds
// should be plenty for this case which is not expected to happen under
// normal circumstances.
// Note that we can't check success at this stage because it is captured
// and we would use a stale value. So we run the timeout unconditionally
// and let the useEffect cancel it if success turns true. Worst case it
// does an API call and then it's a no-op.
if (checkCompleted && !success) {
// Start retries
setRetryCount(1);
retryTimeoutId = setTimeout(async () => {
await checkForTransactions();
retryTimeoutId = null;
}, POLL_REQUEST_DELAY);
}
};

document.addEventListener('visibilitychange', handleVisibilityChange);

return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [to, thisPaymentId, success, disablePaymentId]);

// Retry mechanism: check every second if payment hasn't succeeded yet
useEffect(() => {

if (retryCount === 0 || success || retryCount >= POLL_MAX_RETRY) {
// Retry up to 5 times or until the payment succeeds. If the payment tx
// is not found within this time period, something has gone wrong.
return;
}

const intervalId = setInterval(async () => {
if (success) {
// Stop retries upon success
setRetryCount(0);
return;
if (retryTimeoutId) {
clearTimeout(retryTimeoutId);
retryTimeoutId = null;
}

await checkForTransactions();

// Increment retry count for next attempt (regardless of success/error)
setRetryCount(prev => prev + 1);
}, POLL_REQUEST_DELAY);

return () => {
clearInterval(intervalId);
};
}, [retryCount, success]);
}, [to, thisPaymentId, success, disablePaymentId, checkForTransactions]);

return (
<React.Fragment>
Expand Down
2 changes: 1 addition & 1 deletion react/lib/util/chronik.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChronikClient, WsEndpoint, Tx, ConnectionStrategy} from 'chronik-client-cashtokens';
import { ChronikClient, WsEndpoint, Tx, ConnectionStrategy} from 'chronik-client';
import { encodeCashAddress, decodeCashAddress } from 'ecashaddrjs'
import { AddressType } from 'ecashaddrjs/dist/types'
import xecaddr from 'xecaddrjs'
Expand Down
3 changes: 1 addition & 2 deletions react/lib/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,4 @@ export const DEFAULT_MINIMUM_DONATION_AMOUNT: { [key: string]: number } = {
};

export const POLL_TX_HISTORY_LOOKBACK = 5 // request last 5 txs
export const POLL_REQUEST_DELAY = 1000 // 1s
export const POLL_MAX_RETRY = 5
export const POLL_REQUEST_DELAY = 2000 // 2s
57 changes: 3 additions & 54 deletions react/lib/util/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,10 @@ import { io, Socket } from 'socket.io-client';
import { AltpaymentCoin, AltpaymentError, AltpaymentPair, AltpaymentShift } from '../altpayment';
import config from '../paybutton-config.json';

import { BroadcastTxData, CheckSuccessInfo, Transaction } from './types';
import { getAddressDetails } from './api-client';
import { getAddressPrefixed } from './address';
import { CheckSuccessInfo, Transaction } from './types';
import { shouldTriggerOnSuccess } from './validate';
import { initializeChronikWebsocket } from './chronik';

const txsListener = (txsSocket: Socket, setNewTxs: Function, setDialogOpen?: Function, checkSuccessInfo?: CheckSuccessInfo): void => {
txsSocket.on('incoming-txs', (broadcastedTxData: BroadcastTxData) => {
const unconfirmedTxs = broadcastedTxData.txs.filter(
tx => tx.confirmed === false,
);
if (
broadcastedTxData.messageType === 'NewTx' &&
unconfirmedTxs.length !== 0
) {
if (setDialogOpen !== undefined && checkSuccessInfo !== undefined) {
for (const tx of unconfirmedTxs) {
if (shouldTriggerOnSuccess(
tx,
checkSuccessInfo.currency,
checkSuccessInfo.price,
checkSuccessInfo.randomSatoshis,
checkSuccessInfo.disablePaymentId,
checkSuccessInfo.expectedPaymentId,
checkSuccessInfo.expectedAmount,
checkSuccessInfo.expectedOpReturn,
checkSuccessInfo.currencyObj
)) {
setDialogOpen(true)
setTimeout(() => {
setNewTxs(unconfirmedTxs);
}, 700);
break
}
}
} else {
setNewTxs(unconfirmedTxs);
}
}
});
};
import { WsEndpoint } from 'chronik-client';

interface AltpaymentListenerParams {
addressType: string
Expand Down Expand Up @@ -115,7 +78,7 @@ export const setupAltpaymentSocket = async (params: SetupAltpaymentSocketParams)

interface SetupTxsSocketParams {
address: string
txsSocket?: Socket
txsSocket?: Socket | WsEndpoint
apiBaseUrl?: string
wsBaseUrl?: string
setTxsSocket: Function
Expand All @@ -124,20 +87,6 @@ interface SetupTxsSocketParams {
checkSuccessInfo?: CheckSuccessInfo
}

export const setupTxsSocket = async (params: SetupTxsSocketParams): Promise<void> => {
void getAddressDetails(params.address, params.apiBaseUrl);
if (params.txsSocket !== undefined) {
params.txsSocket.disconnect();
params.setTxsSocket(undefined);
}
const newSocket = io(`${params.wsBaseUrl ?? config.wsBaseUrl}/addresses`, {
forceNew: true,
query: { addresses: [getAddressPrefixed(params.address)] },
});
params.setTxsSocket(newSocket);
txsListener(newSocket, params.setNewTxs, params.setDialogOpen, params.checkSuccessInfo);
}

export const setupChronikWebSocket = async (params: SetupTxsSocketParams): Promise<void> => {
if (params.txsSocket !== undefined) {
console.log(`Closing existing Chronik WebSocket for address: ${params.address}`);
Expand Down
4 changes: 2 additions & 2 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"axios": "1.12.0",
"bignumber.js": "9.0.2",
"cashtab-connect": "^1.1.0",
"chronik-client-cashtokens": "^3.4.0",
"chronik-client": "^4.1.0",
"copy-to-clipboard": "3.3.3",
"crypto-js": "^4.2.0",
"decimal.js": "^10.6.0",
Expand All @@ -118,7 +118,7 @@
"resolutions": {
"@types/react": "18.3.26",
"@types/react-dom": "18.3.0",
"chronik-client-cashtokens/ecashaddrjs": "^2.0.0",
"chronik-client/ecashaddrjs": "^2.0.0",
"webpack": "^5.0.0",
"@babel/plugin-syntax-flow": "^7.14.5",
"@babel/plugin-transform-react-jsx": "^7.14.9",
Expand Down
21 changes: 11 additions & 10 deletions react/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5984,15 +5984,16 @@ chrome-trace-event@^1.0.2:
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b"
integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==

chronik-client-cashtokens@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/chronik-client-cashtokens/-/chronik-client-cashtokens-3.4.0.tgz#0c958c72c119867d924c0c446e0765381fd1b792"
integrity sha512-D2G8j+dhOlG0wr+DtgnMHHwCYzpa/XCzNvHhkkAfOOL3OaAoXONMxhbqx+N1y7B0L5ot69y8z/8KOEz9ARL3hg==
chronik-client@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chronik-client/-/chronik-client-4.1.0.tgz#f49a07565064335f1d576230bf77546a9bc29e8b"
integrity sha512-RA7gXMTmWW3ek29ACvHGfcJxGUBMxtcvMCCV7EJLywYCCYyCDmjdcTVIHEqEFF0QHj6F5PJi5nvA65cAtBnr5Q==
dependencies:
"@types/ws" "^8.2.1"
axios "^1.6.3"
ecashaddrjs "2.0.0"
ecashaddrjs "^2.0.0"
isomorphic-ws "^4.0.1"
long "^4.0.0"
protobufjs "^6.8.8"
ws "^8.3.0"

Expand Down Expand Up @@ -7079,11 +7080,6 @@ eastasianwidth@^0.2.0:
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==

ecashaddrjs@2.0.0, ecashaddrjs@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-2.0.0.tgz#d45ede7fb6168815dbcf664b8e0a6872e485d874"
integrity sha512-EvK1V4D3+nIEoD0ggy/b0F4lW39/72R9aOs/scm6kxMVuXu16btc+H74eQv7okNfXaQWKgolEekZkQ6wfcMMLw==

ecashaddrjs@^1.0.7:
version "1.6.2"
resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-1.6.2.tgz#9fec1131bc006a874e4c2fa589e6fc880c40745e"
Expand All @@ -7092,6 +7088,11 @@ ecashaddrjs@^1.0.7:
big-integer "1.6.36"
bs58check "^3.0.1"

ecashaddrjs@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-2.0.0.tgz#d45ede7fb6168815dbcf664b8e0a6872e485d874"
integrity sha512-EvK1V4D3+nIEoD0ggy/b0F4lW39/72R9aOs/scm6kxMVuXu16btc+H74eQv7okNfXaQWKgolEekZkQ6wfcMMLw==

ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
Expand Down