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
15 changes: 10 additions & 5 deletions src/components/Payment/Views/Confirm.payment.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export default function ConfirmPaymentView({
chainId: fromChainId,
},
usdAmount,
disableCoral: isAddMoneyFlow && isUsingExternalWallet,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just isUsingExternalWallet? that should be sufficient for knowing we dont want expiry

btw: this are good names and its very easy to reason about!

})
}
}, [
Expand All @@ -197,6 +198,8 @@ export default function ConfirmPaymentView({
isDirectUsdPayment,
wagmiAddress,
peanutWalletAddress,
isAddMoneyFlow,
isUsingExternalWallet,
])

useEffect(() => {
Expand Down Expand Up @@ -232,7 +235,8 @@ export default function ConfirmPaymentView({
}, [chargeDetails, selectedTokenData, selectedChainID])

const routeTypeError = useMemo((): string | null => {
if (!isCrossChainPayment || !xChainRoute || !isPeanutWallet) return null
// error only applies to peanut wallet flows (not external wallet) where cross-chain swap route is RFQ-required.
if (!isCrossChainPayment || !xChainRoute || isUsingExternalWallet || !isPeanutWallet) return null

// For peanut wallet flows, only RFQ routes are allowed
if (xChainRoute.type === 'swap') {
Expand All @@ -247,7 +251,7 @@ export default function ConfirmPaymentView({
}

return null
}, [isCrossChainPayment, xChainRoute, isPeanutWallet])
}, [isCrossChainPayment, xChainRoute, isUsingExternalWallet, isPeanutWallet])

const errorMessage = useMemo((): string | undefined => {
if (isRouteExpired) return 'This quoute has expired. Please retry to fetch latest quote.'
Expand Down Expand Up @@ -424,7 +428,7 @@ export default function ConfirmPaymentView({
tokenSymbol={symbolForDisplay}
message={chargeDetails?.requestLink?.reference ?? ''}
fileUrl={chargeDetails?.requestLink?.attachmentUrl ?? ''}
showTimer={isCrossChainPayment}
showTimer={isCrossChainPayment && xChainRoute?.type === 'rfq'}
timerExpiry={xChainRoute?.expiry}
isTimerLoading={isCalculatingFees || isPreparingTx}
onTimerNearExpiry={handleRouteRefresh}
Expand Down Expand Up @@ -485,12 +489,13 @@ export default function ConfirmPaymentView({
}
/>
)}

{/* note: @dev temp hide gas fee, estimation is way off */}
{/*
<PaymentInfoRow
loading={isCalculatingFees || isEstimatingGas || isPreparingTx}
label={'Network fee'}
value={networkFee}
/>
/> */}

<PaymentInfoRow hideBottomBorder label="Peanut fee" value={`$ 0.00`} />
</Card>
Expand Down
6 changes: 4 additions & 2 deletions src/constants/general.consts.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import * as interfaces from '@/interfaces'
import { CHAIN_DETAILS, TOKEN_DETAILS } from '@squirrel-labs/peanut-sdk'
import { mainnet, arbitrum, arbitrumSepolia, polygon, optimism, base, bsc, scroll, baseSepolia } from 'viem/chains'
import { mainnet, arbitrum, arbitrumSepolia, polygon, optimism, base, bsc, scroll } from 'viem/chains'

export const peanutWalletIsInPreview = true

export const INFURA_API_KEY = process.env.NEXT_PUBLIC_INFURA_API_KEY
export const ALCHEMY_API_KEY = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY

export const SQUID_INTEGRATOR_ID = process.env.SQUID_INTEGRATOR_ID!
export const SQUID_API_URL = process.env.SQUID_API_URL
export const DEFAULT_SQUID_INTEGRATOR_ID =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bit sketch?

process.env.DEFAULT_SQUID_INTEGRATOR_ID || process.env.NEXT_PUBLIC_DEFAULT_SQUID_INTEGRATOR_ID
export const SQUID_API_URL = process.env.NEXT_PUBLIC_SQUID_API_URL

const infuraUrl = (subdomain: string) => (INFURA_API_KEY ? `https://${subdomain}.infura.io/v3/${INFURA_API_KEY}` : null)
const alchemyUrl = (subdomain: string) =>
Expand Down
26 changes: 17 additions & 9 deletions src/hooks/usePaymentInitiator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export const usePaymentInitiator = () => {
chargeDetails,
from,
usdAmount,
disableCoral = false,
}: {
chargeDetails: TRequestChargeResponse
from: {
Expand All @@ -190,6 +191,7 @@ export const usePaymentInitiator = () => {
chainId: string
}
usdAmount?: string
disableCoral?: boolean
}) => {
setError(null)
setIsFeeEstimationError(false)
Expand All @@ -216,15 +218,18 @@ export const usePaymentInitiator = () => {
: {
toAmount: parseUnits(chargeDetails.tokenAmount, chargeDetails.tokenDecimals),
}
const xChainRoute = await getRoute({
from,
to: {
address: chargeDetails.requestLink.recipientAddress as Address,
tokenAddress: chargeDetails.tokenAddress as Address,
chainId: chargeDetails.chainId,
const xChainRoute = await getRoute(
{
from,
to: {
address: chargeDetails.requestLink.recipientAddress as Address,
tokenAddress: chargeDetails.tokenAddress as Address,
chainId: chargeDetails.chainId,
},
...amount,
},
...amount,
})
{ disableCoral }
)

const slippagePercentage = Number(xChainRoute.fromAmount) / Number(chargeDetails.tokenAmount) - 1
setXChainRoute(xChainRoute)
Expand Down Expand Up @@ -490,6 +495,7 @@ export const usePaymentInitiator = () => {

// update payment status in the backend api.
setLoadingStep('Updating Payment Status')
// peanut wallet flow: payer is the peanut wallet itself
const payment: PaymentCreationResponse = await chargesApi.createPayment({
chargeId: chargeDetails.uuid,
chainId: PEANUT_WALLET_CHAIN.id.toString(),
Expand All @@ -506,7 +512,7 @@ export const usePaymentInitiator = () => {
console.log('Peanut Wallet payment successful.')
return { status: 'Success', charge: chargeDetails, payment, txHash: receipt.transactionHash, success: true }
},
[sendTransactions, xChainUnsignedTxs, unsignedTx]
[sendTransactions, xChainUnsignedTxs, unsignedTx, peanutWalletAddress]
)

// helper function: Handle External Wallet payment
Expand Down Expand Up @@ -596,6 +602,7 @@ export const usePaymentInitiator = () => {

setLoadingStep('Updating Payment Status')
console.log('Updating payment status in backend for external wallet. Hash:', txHash)
// external wallet / add-money flow: payer is the connected wallet address
const payment = await chargesApi.createPayment({
chargeId: chargeDetails.uuid,
chainId: sourceChainId.toString(),
Expand All @@ -621,6 +628,7 @@ export const usePaymentInitiator = () => {
sendTransactionAsync,
config,
selectedTokenData,
wagmiAddress,
]
)

Expand Down
81 changes: 55 additions & 26 deletions src/services/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ type SquidGetRouteParams = {
toToken: string
}

// options for route fetching behaviour
type RouteOptions = {
/**
* when true, we will fetch a route using a non-Coral Squid integrator id.
* coral (rfq) routes have a very short expiry which is problematic for
* external-wallet Add Money flows that require approval + swap. Setting
* this flag disables coral by switching the integrator key.
*/
disableCoral?: boolean
}

type SquidCall = {
chainType: string
callType: number
Expand Down Expand Up @@ -240,12 +251,17 @@ async function estimateApprovalCostUsd(
* Fetch the route from the squid API.
* We use this when we fetch the route several times while finding the optimal fromAmount.
*/
async function getSquidRouteRaw(params: SquidGetRouteParams): Promise<SquidRouteResponse> {
async function getSquidRouteRaw(params: SquidGetRouteParams, options: RouteOptions = {}): Promise<SquidRouteResponse> {
const response = await fetchWithSentry(`${SQUID_API_URL}/v2/route`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-integrator-id': process.env.SQUID_INTEGRATOR_ID!,
// use alternative integrator id when coral must be disabled
'x-integrator-id': options.disableCoral
? process.env.DEFAULT_SQUID_INTEGRATOR_ID ||
process.env.NEXT_PUBLIC_DEFAULT_SQUID_INTEGRATOR_ID ||
process.env.SQUID_INTEGRATOR_ID!
: process.env.SQUID_INTEGRATOR_ID!,
},
body: JSON.stringify(params),
})
Expand All @@ -268,7 +284,8 @@ async function getSquidRouteRaw(params: SquidGetRouteParams): Promise<SquidRoute
async function findOptimalFromAmount(
params: Omit<SquidGetRouteParams, 'fromAmount'>,
targetToAmount: bigint,
_toTokenPrice?: { price: number; decimals: number }
_toTokenPrice?: { price: number; decimals: number },
options: RouteOptions = {}
): Promise<SquidRouteResponse> {
// Only fetch if not provided
const [toTokenPrice, fromTokenPrice] = await Promise.all([
Expand Down Expand Up @@ -318,7 +335,7 @@ async function findOptimalFromAmount(
const testParams = { ...params, fromAmount: midPoint.toString() }

try {
const response = await getSquidRouteRaw(testParams)
const response = await getSquidRouteRaw(testParams, options)
const receivedAmount = BigInt(response.route.estimate.toAmountMin)
console.log('fromAmount', midPoint)
console.log('receivedAmount', receivedAmount)
Expand Down Expand Up @@ -358,7 +375,7 @@ async function findOptimalFromAmount(
}

// Fallback call
return await getSquidRouteRaw({ ...params, fromAmount: highBound.toString() })
return await getSquidRouteRaw({ ...params, fromAmount: highBound.toString() }, options)
}

export type PeanutCrossChainRoute = {
Expand All @@ -381,23 +398,29 @@ export type PeanutCrossChainRoute = {
*
* Returns the route with the less slippage..
*/
export async function getRoute({ from, to, ...amount }: RouteParams): Promise<PeanutCrossChainRoute> {
export async function getRoute(
{ from, to, ...amount }: RouteParams,
options: RouteOptions = {}
): Promise<PeanutCrossChainRoute> {
let fromAmount: string
let response: SquidRouteResponse

console.info('getRoute', { from, to }, amount)

if (amount.fromAmount) {
fromAmount = amount.fromAmount.toString()
response = await getSquidRouteRaw({
fromChain: from.chainId,
fromToken: from.tokenAddress,
fromAmount: fromAmount,
fromAddress: from.address,
toAddress: to.address,
toChain: to.chainId,
toToken: to.tokenAddress,
})
response = await getSquidRouteRaw(
{
fromChain: from.chainId,
fromToken: from.tokenAddress,
fromAmount: fromAmount,
fromAddress: from.address,
toAddress: to.address,
toChain: to.chainId,
toToken: to.tokenAddress,
},
options
)
} else if (amount.fromUsd) {
// Convert USD to token amount
const fromTokenPrice = await fetchTokenPrice(from.tokenAddress, from.chainId)
Expand All @@ -406,15 +429,18 @@ export async function getRoute({ from, to, ...amount }: RouteParams): Promise<Pe
const tokenAmount = Number(amount.fromUsd) / fromTokenPrice.price
fromAmount = parseUnits(tokenAmount.toFixed(fromTokenPrice.decimals), fromTokenPrice.decimals).toString()

response = await getSquidRouteRaw({
fromChain: from.chainId,
fromToken: from.tokenAddress,
fromAmount,
fromAddress: from.address,
toAddress: to.address,
toChain: to.chainId,
toToken: to.tokenAddress,
})
response = await getSquidRouteRaw(
{
fromChain: from.chainId,
fromToken: from.tokenAddress,
fromAmount,
fromAddress: from.address,
toAddress: to.address,
toChain: to.chainId,
toToken: to.tokenAddress,
},
options
)
} else if (amount.toAmount) {
// Use binary search to find optimal fromAmount
response = await findOptimalFromAmount(
Expand All @@ -426,7 +452,9 @@ export async function getRoute({ from, to, ...amount }: RouteParams): Promise<Pe
toChain: to.chainId,
toToken: to.tokenAddress,
},
amount.toAmount
amount.toAmount,
undefined /* _toTokenPrice */,
options
)
} else if (amount.toUsd) {
// Convert target USD to token amount, then use binary search
Expand All @@ -448,7 +476,8 @@ export async function getRoute({ from, to, ...amount }: RouteParams): Promise<Pe
toToken: to.tokenAddress,
},
targetToAmount,
toTokenPrice // Pass the already-fetched price
toTokenPrice, // Pass the already-fetched price
options
)
} else {
throw new Error('No amount specified')
Expand Down
Loading