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

New wrap #200

Merged
merged 15 commits into from
Mar 15, 2025
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
"scripts": {
"start": "vite --host",
"dev": "vite --host",
"build": "tsc && NODE_OPTIONS='--max-old-space-size=8192' vite build",
"build": "tsc && vite build",
"preview": "vite preview",
"prepare": "husky install"
},
29 changes: 24 additions & 5 deletions src/components/BalanceUI.tsx
Original file line number Diff line number Diff line change
@@ -15,13 +15,17 @@ interface IProps {
chain?: Chain
isSecretToken?: boolean
showBalanceLabel?: boolean
onBalanceClick?: any
showCurrencyEquiv?: boolean
}

export default function BalanceUI({
token,
chain = chains['Secret Network'],
isSecretToken = false,
showBalanceLabel = true
showBalanceLabel = true,
onBalanceClick = false,
showCurrencyEquiv = true
}: IProps) {
const {
isConnected,
@@ -88,7 +92,7 @@ export default function BalanceUI({

return (
<>
<div className="flex items-center gap-1.5">
<div className="flex items-center justify-center gap-1.5">
{showBalanceLabel && <span className="font-bold">{`Balance: `}</span>}

{/* Skeleton Loader */}
@@ -99,18 +103,33 @@ export default function BalanceUI({
{balance !== null &&
balance !== ('viewingKeyError' as GetBalanceError) &&
balance !== ('GenericFetchError' as GetBalanceError) &&
token.name && (
token.name &&
(onBalanceClick ? (
<span
className="cursor-pointer hover:underline"
onClick={() => onBalanceClick(Number(BigNumber(balance).dividedBy(`1e${token.decimals}`)))} // ADD
>
<span className="font-medium font-mono">{` ${Number(
BigNumber(balance).dividedBy(`1e${token.decimals}`)
).toLocaleString(undefined, {
maximumFractionDigits: token.decimals
})}
${token.name == 'SCRT' && isSecretToken ? 's' : ''}${token.name} ${
token.coingecko_id && currencyPriceString && showCurrencyEquiv ? ` (${currencyPriceString})` : ''
}`}</span>
</span>
) : (
<>
<span className="font-medium font-mono">{` ${Number(
BigNumber(balance).dividedBy(`1e${token.decimals}`)
).toLocaleString(undefined, {
maximumFractionDigits: token.decimals
})}
${token.name == 'SCRT' && isSecretToken ? 's' : ''}${token.name} ${
token.coingecko_id && currencyPriceString ? ` (${currencyPriceString})` : ''
token.coingecko_id && currencyPriceString && showCurrencyEquiv ? ` (${currencyPriceString})` : ''
}`}</span>
</>
)}
))}

{balance === ('viewingKeyError' as GetBalanceError) && (
<button
2 changes: 1 addition & 1 deletion src/components/FeeGrant/FeeGrant.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import ActionableStatus from './components/ActionableStatus'

export default function FeeGrant() {
return (
<div className="bg-gray-200 dark:bg-neutral-700 text-black dark:text-white p-4 rounded-xl select-none flex items-center">
<div className="bg-gray-200 dark:bg-neutral-700 text-black dark:text-white p-4 rounded-xl select-none flex items-center gap-4">
<div className="flex-1">
<Tooltip
title={`Request Fee Grant so that you don't have to pay gas fees (up to 0.1 SCRT)`}
35 changes: 3 additions & 32 deletions src/pages/wrap/Wrap.tsx
Original file line number Diff line number Diff line change
@@ -4,18 +4,13 @@ import { wrapPageTitle, wrapPageDescription, wrapJsonLdSchema } from 'utils/comm
import { Helmet } from 'react-helmet-async'
import FeeGrantInfoModal from './components/FeeGrantInfoModal'
import mixpanel from 'mixpanel-browser'
import { useSearchParams } from 'react-router-dom'
import { WrappingMode, isWrappingMode } from 'types/WrappingMode'
import { useSecretNetworkClientStore } from 'store/secretNetworkClient'
import Title from 'components/Title'
import WrapForm from './components/WrapForm'
import SCRTUnwrapWarning from './components/SCRTUnwrapWarning'

export function Wrap() {
const secretToken: Token = tokens.find((token) => token.name === 'SCRT')
const [selectedToken, setSelectedToken] = useState<Token>(secretToken)
const [wrappingMode, setWrappingMode] = useState<WrappingMode>('wrap')

const { getBalance } = useSecretNetworkClientStore()
const scrtBalance = getBalance(
tokens.find((token) => token.name === 'SCRT'),
@@ -32,31 +27,7 @@ export function Wrap() {
}
}, [])

// URL params
const [searchParams, setSearchParams] = useSearchParams()
const modeUrlParam = searchParams.get('mode')
const tokenUrlParam = searchParams.get('token')

const isValidTokenParam = () => {
return tokens.find((token: Token) => token.name.toLowerCase() === tokenUrlParam.toLowerCase()) ? true : false
}

useEffect(() => {
if (isWrappingMode(modeUrlParam?.toLowerCase())) {
setWrappingMode(modeUrlParam.toLowerCase() as WrappingMode)
}
}, [modeUrlParam])

useEffect(() => {
if (tokenUrlParam && isValidTokenParam()) {
setSelectedToken(tokens.find((token: Token) => token.name.toLowerCase() === tokenUrlParam.toLowerCase()))
}
}, [tokenUrlParam])

const infoMsg =
wrappingMode === 'wrap'
? `Converting publicly visible ${selectedToken.name} into its privacy-preserving equivalent Secret ${selectedToken.name}. These tokens are not publicly visible and require a viewing key`
: `Converting privacy-preserving Secret ${selectedToken.name} into its publicly visible equivalent ${selectedToken.name}`
const infoMsg = `Converting publicly visible tokens into its privacy-preserving equivalent Secret Token and vice versa. These tokens are not publicly visible and require a viewing key.`

const [isFeeGrantInfoModalOpen, setIsFeeGrantInfoModalOpen] = useState(false)

@@ -93,9 +64,9 @@ export function Wrap() {
/>

{/* Content */}
<div className="container w-full max-w-xl mx-auto px-4">
<div className="container w-full max-w-5xl mx-auto px-4">
{/* Title: Secret Wrap / Secret Unwrap */}
<Title className="mb-6" title={`Secret ${wrappingMode === 'wrap' ? 'Wrap' : 'Unwrap'}`} tooltip={infoMsg} />
<Title className="mb-6" title={`Secret Wrap`} tooltip={infoMsg} />
{Number(scrtBalance) === 0 && scrtBalance !== null ? <SCRTUnwrapWarning /> : null}
{/* Content */}
<div className="rounded-3xl px-6 py-6 bg-white border border-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
945 changes: 602 additions & 343 deletions src/pages/wrap/components/WrapForm.tsx

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/pages/wrap/components/wrap.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.no-spinner::-webkit-inner-spin-button,
.no-spinner::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

.no-spinner {
-moz-appearance: textfield;
}
27 changes: 0 additions & 27 deletions src/pages/wrap/wrapSchema.ts

This file was deleted.

113 changes: 112 additions & 1 deletion src/services/wrap.service.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,21 @@ interface IPropsTokenName extends IBaseProps {
tokenName: string
}

interface IBatchItem {
token: Token
amount: string
wrappingMode: WrappingMode
}

/**
* Batch props
*/
interface IBatchProps {
secretNetworkClient: SecretNetworkClient
feeGrantStatus: FeeGrantStatus
items: IBatchItem[]
}

type TProps = IPropsToken | IPropsTokenName

async function performWrapping(props: TProps): Promise<string> {
@@ -105,7 +120,103 @@ async function performWrapping(props: TProps): Promise<string> {
})
return
}
/**
* Perform a single broadcast with multiple wrap/unwrap messages
*/
export async function performBatchWrapping(props: IBatchProps): Promise<void> {
if (!props.items?.length) {
throw new Error('No items to wrap or unwrap.')
}

// Build an array of messages
const messages: any[] = []

// Create each MsgExecuteContract
for (const batchItem of props.items) {
// Convert “display” amount to on-chain integer
const bigAmount = new BigNumber(batchItem.amount)
const baseAmount = bigAmount.multipliedBy(`1e${batchItem.token.decimals}`).toFixed(0, BigNumber.ROUND_DOWN)

if (baseAmount === 'NaN') {
throw new Error(`Invalid amount: ${batchItem.amount}`)
}

if (batchItem.wrappingMode === 'wrap') {
// Wrap
messages.push(
new MsgExecuteContract({
sender: props.secretNetworkClient.address,
contract_address: batchItem.token.address,
code_hash: batchItem.token.code_hash,
sent_funds: [
{
denom: batchItem.token.withdrawals[0].denom,
amount: baseAmount
}
],
msg: {
deposit: {
padding: randomPadding()
}
}
})
)
} else {
// Unwrap
messages.push(
new MsgExecuteContract({
sender: props.secretNetworkClient.address,
contract_address: batchItem.token.address,
code_hash: batchItem.token.code_hash,
sent_funds: [],
msg: {
redeem: {
amount: baseAmount,
// For SCRT, denom is optional
denom: batchItem.token.name === 'SCRT' ? undefined : batchItem.token.withdrawals[0].denom,
padding: randomPadding()
}
}
})
)
}
}

// Broadcast all messages in a single transaction
const broadcastResult = await props.secretNetworkClient.tx.broadcast(messages, {
gasLimit: 50_000 + 40_000 * messages.length,
gasPriceInFeeDenom: 0.25,
feeDenom: 'uscrt',
feeGranter: props.feeGrantStatus === 'success' ? faucetAddress : '',
broadcastMode: BroadcastMode.Sync,
waitForCommit: false
})

// Poll for the transaction result
await queryTxResult(
props.secretNetworkClient,
broadcastResult.transactionHash,
6_000, // poll interval
10 // number of retries
)
.catch((error: any) => {
// If we fail to find or confirm the tx
if (error?.tx?.rawLog) {
throw new Error(`${error.tx.rawLog}`)
} else {
throw error
}
})
.then((tx: any) => {
if (tx) {
if (tx.code !== 0) {
throw new Error(tx.rawLog || 'Batch wrap error')
}
}
})
}

export const WrapService = {
performWrapping
performWrapping,
performBatchWrapping
}
4 changes: 3 additions & 1 deletion src/store/secretNetworkClient.ts
Original file line number Diff line number Diff line change
@@ -110,7 +110,9 @@ export const useSecretNetworkClientStore = create<SecretNetworkClientState>()((s
walletAddress: walletAddress,
tokens: allTokens
})
set({ balanceMapping: balances })
if (balances != null) {
set({ balanceMapping: balances })
}
},
getBalance(token: Token, secretToken: boolean = false) {
if (!get().isInitialized) {