Skip to content

Commit 81ab59c

Browse files
committed
feat: L6 production patterns + L7 USDC permit
L6 updates: - use getRequiredRelayGas() from contract - Uniswap V3 quoter for POL/USDT pricing - reduce lookback 10h→1h L7 new: - USDC EIP-2612 permit approval - transferWithPermit vs transferWithApproval - version-based domain separator
1 parent 6f5d8bb commit 81ab59c

File tree

7 files changed

+1073
-63
lines changed

7 files changed

+1073
-63
lines changed

src/content/tutorial/6-gasless-transfers/6-optimized-fees/_solution/index.js

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ const RECEIVER_ADDRESS = '0xA3E49ef624bEaC43D29Af86bBFdE975Abaa0E184'
1414

1515
const TRANSFER_AMOUNT_USDT = '0.01'
1616

17-
// Gas limits by method (from Nimiq wallet)
18-
const GAS_LIMITS = {
19-
transfer: 65000,
20-
transferWithPermit: 72000,
21-
transferWithApproval: 72000,
22-
swapWithApproval: 85000,
23-
}
17+
// Uniswap V3 addresses for price queries
18+
const UNISWAP_QUOTER = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'
19+
const WMATIC_ADDRESS = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'
20+
const USDT_WMATIC_POOL = '0x9B08288C3Be4F62bbf8d1C20Ac9C5e6f9467d8B7'
21+
22+
// Method selector for transferWithApproval
23+
const METHOD_SELECTOR_TRANSFER_WITH_APPROVAL = '0x8d89149b'
2424

2525
// ABIs
2626
const USDT_ABI = [
@@ -36,13 +36,20 @@ const TRANSFER_ABI = [
3636

3737
const RELAY_HUB_ABI = [
3838
'event RelayServerRegistered(address indexed relayManager, uint256 baseRelayFee, uint256 pctRelayFee, string relayUrl)',
39-
'function calldataGasCost(uint256) view returns (uint256)',
39+
]
40+
41+
const UNISWAP_POOL_ABI = [
42+
'function fee() external view returns (uint24)',
43+
]
44+
45+
const UNISWAP_QUOTER_ABI = [
46+
'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)',
4047
]
4148

4249
async function discoverRelays(provider) {
4350
const relayHub = new ethers.Contract(RELAY_HUB_ADDRESS, RELAY_HUB_ABI, provider)
4451
const currentBlock = await provider.getBlockNumber()
45-
const LOOKBACK_BLOCKS = 14400 // ~10 hours
52+
const LOOKBACK_BLOCKS = 1600 // ~1 hour (2s per block)
4653

4754
const events = await relayHub.queryFilter(
4855
relayHub.filters.RelayServerRegistered(),
@@ -107,7 +114,27 @@ async function validateRelay(relay, provider) {
107114
}
108115
}
109116

110-
async function calculateOptimalFee(relay, provider, method = 'transferWithApproval', isMainnet = true) {
117+
async function getPolUsdtPrice(provider) {
118+
// Query Uniswap V3 pool for USDT/WMATIC price
119+
const pool = new ethers.Contract(USDT_WMATIC_POOL, UNISWAP_POOL_ABI, provider)
120+
const quoter = new ethers.Contract(UNISWAP_QUOTER, UNISWAP_QUOTER_ABI, provider)
121+
122+
// Get pool fee tier
123+
const fee = await pool.fee()
124+
125+
// Quote: How much POL for 1 USDT (1_000_000 base units)?
126+
const polAmountOut = await quoter.callStatic.quoteExactInputSingle(
127+
USDT_ADDRESS, // tokenIn (USDT)
128+
WMATIC_ADDRESS, // tokenOut (WMATIC)
129+
fee, // pool fee
130+
ethers.utils.parseUnits('1', 6), // 1 USDT
131+
0, // sqrtPriceLimitX96
132+
)
133+
134+
return polAmountOut // POL wei per 1 USDT
135+
}
136+
137+
async function calculateOptimalFee(relay, provider, transferContract, isMainnet = true) {
111138
// Step 1: Get network gas price
112139
const networkGasPrice = await provider.getGasPrice()
113140

@@ -123,10 +150,10 @@ async function calculateOptimalFee(relay, provider, method = 'transferWithApprov
123150

124151
console.log(' Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'gwei'), 'gwei', `(${bufferPercentage}%)`)
125152

126-
// Step 4: Get gas limit for method
127-
const gasLimit = GAS_LIMITS[method]
153+
// Step 4: Get gas limit from transfer contract
154+
const gasLimit = await transferContract.getRequiredRelayGas(METHOD_SELECTOR_TRANSFER_WITH_APPROVAL)
128155

129-
console.log(' Gas limit:', gasLimit)
156+
console.log(' Gas limit:', gasLimit.toString())
130157

131158
// Step 5: Calculate base cost
132159
const baseCost = bufferedGasPrice.mul(gasLimit)
@@ -140,12 +167,13 @@ async function calculateOptimalFee(relay, provider, method = 'transferWithApprov
140167
console.log(' Total POL cost:', ethers.utils.formatEther(totalPOLCost), 'POL')
141168
console.log(' Relay fee:', `${relay.pctRelayFee}%`)
142169

143-
// Step 8: Convert to USDT (conservative rate: $0.50 per POL)
144-
const POL_PRICE_USD = 0.50 // Conservative estimate
145-
const feeInUSD = Number.parseFloat(ethers.utils.formatEther(totalPOLCost)) * POL_PRICE_USD
170+
// Step 8: Get real-time POL/USDT price from Uniswap
171+
const polPerUsdt = await getPolUsdtPrice(provider)
172+
console.log(' Uniswap rate:', ethers.utils.formatEther(polPerUsdt), 'POL per USDT')
146173

147-
// Step 9: Apply 10% safety buffer for USDT conversion
148-
const feeInUSDT = ethers.utils.parseUnits((feeInUSD * 1.10).toFixed(6), 6)
174+
// Step 9: Convert POL fee to USDT with 10% buffer
175+
// totalPOLCost (POL wei) / polPerUsdt (POL wei per USDT) = USDT base units
176+
const feeInUSDT = totalPOLCost.mul(1_000_000).div(polPerUsdt).mul(110).div(100)
149177

150178
console.log(' USDT fee:', ethers.utils.formatUnits(feeInUSDT, 6), 'USDT')
151179

@@ -157,7 +185,7 @@ async function calculateOptimalFee(relay, provider, method = 'transferWithApprov
157185
}
158186
}
159187

160-
async function findBestRelay(provider) {
188+
async function findBestRelay(provider, transferContract) {
161189
console.log('\n🔍 Discovering relays...')
162190
const relays = await discoverRelays(provider)
163191
console.log(`Found ${relays.length} unique relay URLs`)
@@ -176,7 +204,7 @@ async function findBestRelay(provider) {
176204
console.log(`📊 ${relay.url}`)
177205

178206
try {
179-
const feeData = await calculateOptimalFee(validRelay, provider)
207+
const feeData = await calculateOptimalFee(validRelay, provider, transferContract)
180208

181209
if (feeData.usdtFee.lt(lowestFee)) {
182210
lowestFee = feeData.usdtFee
@@ -208,8 +236,11 @@ async function main() {
208236
console.log('🔑 Sender:', wallet.address)
209237
console.log('📍 Receiver:', RECEIVER_ADDRESS)
210238

239+
// Setup transfer contract
240+
const transferContract = new ethers.Contract(TRANSFER_CONTRACT_ADDRESS, TRANSFER_ABI, provider)
241+
211242
// Find best relay with optimized fee
212-
const relay = await findBestRelay(provider)
243+
const relay = await findBestRelay(provider, transferContract)
213244

214245
console.log('\n✅ Selected relay:', relay.url)
215246
console.log(' Worker:', relay.relayWorkerAddress)
@@ -259,7 +290,6 @@ async function main() {
259290
const { r: sigR, s: sigS, v: sigV } = ethers.utils.splitSignature(approvalSignature)
260291

261292
// Build transfer calldata
262-
const transferContract = new ethers.Contract(TRANSFER_CONTRACT_ADDRESS, TRANSFER_ABI, provider)
263293
const transferCalldata = transferContract.interface.encodeFunctionData('transferWithApproval', [
264294
USDT_ADDRESS,
265295
transferAmount,
@@ -337,9 +367,10 @@ async function main() {
337367
console.log('🔗 View:', `https://polygonscan.com/tx/${txHash}`)
338368
console.log('\n💡 Used production-grade fee optimization:')
339369
console.log(' ✅ Dynamic gas price discovery')
370+
console.log(' ✅ Gas limits from contract (getRequiredRelayGas)')
371+
console.log(' ✅ Real-time POL/USDT pricing from Uniswap V3')
340372
console.log(' ✅ Network-aware safety buffers')
341373
console.log(' ✅ Relay fee comparison')
342-
console.log(' ✅ POL-to-USDT conversion with safety margin')
343374
}
344375

345376
main().catch((error) => {

src/content/tutorial/6-gasless-transfers/6-optimized-fees/content.md

Lines changed: 88 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,35 @@ console.log('Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'g
9090

9191
---
9292

93-
## Step 3: Combine Gas Costs and Relay Fees
94-
95-
```js title="fee-components.ts" showLineNumbers mark=1-18
96-
// Method-specific gas limits (from wallet implementation)
97-
const GAS_LIMITS = {
98-
transfer: 65000,
99-
transferWithPermit: 72000,
100-
transferWithApproval: 72000,
101-
swapWithApproval: 85000
102-
}
93+
## Step 3: Get Gas Limit from Transfer Contract
94+
95+
Instead of hardcoding gas limits, query the transfer contract directly:
96+
97+
```js title="gas-limit.ts" showLineNumbers mark=1-11
98+
const TRANSFER_CONTRACT_ABI = [
99+
'function getRequiredRelayGas(bytes4 methodId) view returns (uint256)'
100+
]
103101

104-
const method = 'transferWithApproval'
105-
const gasLimit = GAS_LIMITS[method]
102+
const transferContract = new ethers.Contract(
103+
TRANSFER_CONTRACT_ADDRESS,
104+
TRANSFER_CONTRACT_ABI,
105+
provider
106+
)
107+
108+
// Method selector for transferWithApproval
109+
const METHOD_SELECTOR = '0x8d89149b'
110+
const gasLimit = await transferContract.getRequiredRelayGas(METHOD_SELECTOR)
111+
112+
console.log('Gas limit:', gasLimit.toString())
113+
```
106114

115+
This ensures your gas estimates stay accurate even if the contract changes.
116+
117+
---
118+
119+
## Step 4: Combine Gas Costs and Relay Fees
120+
121+
```js title="fee-components.ts" showLineNumbers mark=1-14
107122
// Calculate base cost
108123
const baseCost = bufferedGasPrice.mul(gasLimit)
109124

@@ -122,30 +137,57 @@ This yields the amount of POL the relay expects to receive after covering gas.
122137

123138
---
124139

125-
## Step 4: Convert POL Cost to USDT
140+
## Step 5: Get Real-Time POL/USDT Price from Uniswap
141+
142+
Query Uniswap V3 for the current exchange rate:
126143

127-
```js title="fee-to-usdt.ts" showLineNumbers mark=1-11
128-
// Option 1: Hardcoded conservative rate
129-
const POL_PRICE_USD = 0.50 // $0.50 per POL (check current market)
130-
const USDT_DECIMALS = 6
144+
```js title="uniswap-price.ts" showLineNumbers mark=1-21
145+
const UNISWAP_QUOTER = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'
146+
const WMATIC = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'
147+
const USDT_WMATIC_POOL = '0x9B08288C3Be4F62bbf8d1C20Ac9C5e6f9467d8B7'
131148

132-
const feeInUSD = Number.parseFloat(ethers.utils.formatEther(totalChainTokenFee)) * POL_PRICE_USD
133-
const feeInUSDT = ethers.utils.parseUnits((feeInUSD * 1.10).toFixed(6), USDT_DECIMALS) // 10% buffer
149+
const POOL_ABI = ['function fee() external view returns (uint24)']
150+
const QUOTER_ABI = [
151+
'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)'
152+
]
134153

135-
console.log('USDT fee:', ethers.utils.formatUnits(feeInUSDT, USDT_DECIMALS))
154+
async function getPolUsdtPrice(provider) {
155+
const pool = new ethers.Contract(USDT_WMATIC_POOL, POOL_ABI, provider)
156+
const quoter = new ethers.Contract(UNISWAP_QUOTER, QUOTER_ABI, provider)
136157

137-
// Option 2: Fetch from Uniswap pool (advanced)
138-
// const pool = new ethers.Contract(POOL_ADDRESS, POOL_ABI, provider)
139-
// const price = await pool.slot0() // Get current price from pool
158+
const fee = await pool.fee()
159+
160+
// Quote: How much POL for 1 USDT?
161+
const polPerUsdt = await quoter.callStatic.quoteExactInputSingle(
162+
USDT_ADDRESS, WMATIC, fee, ethers.utils.parseUnits('1', 6), 0
163+
)
164+
165+
return polPerUsdt // POL wei per 1 USDT
166+
}
140167
```
141168

142-
Start with a conservative fixed price, then graduate to on-chain oracles once you are ready.
169+
---
170+
171+
## Step 6: Convert POL Cost to USDT
143172

144-
In production we look up the POL/USDT price using Uniswap's Quoter contract and then apply an extra 10-15% margin as insurance. That keeps the relay from being underpaid even if POL appreciates between fee estimation and transaction confirmation.
173+
```js title="fee-to-usdt.ts" showLineNumbers mark=1-8
174+
const polPerUsdt = await getPolUsdtPrice(provider)
175+
176+
// Convert: totalPOLCost / polPerUsdt = USDT units
177+
// Apply 10% buffer for safety
178+
const feeInUSDT = totalChainTokenFee
179+
.mul(1_000_000)
180+
.div(polPerUsdt)
181+
.mul(110).div(100)
182+
183+
console.log('USDT fee:', ethers.utils.formatUnits(feeInUSDT, 6))
184+
```
185+
186+
Using Uniswap ensures your fees reflect current market rates, preventing underpayment when POL appreciates.
145187

146188
---
147189

148-
## Step 5: Reject Expensive Relays
190+
## Step 7: Reject Expensive Relays
149191

150192
```js title="guardrails.ts" showLineNumbers mark=1-9
151193
const MAX_PCT_RELAY_FEE = 70 // Never accept >70%
@@ -164,7 +206,7 @@ These guardrails prevent accidental overpayment when a relay is misconfigured or
164206

165207
---
166208

167-
## Step 6: Choose the Best Relay
209+
## Step 8: Choose the Best Relay
168210

169211
```js title="choose-relay.ts" showLineNumbers mark=1-17
170212
async function getBestRelay(relays, gasLimit, method) {
@@ -208,24 +250,30 @@ These extras come straight from the Nimiq wallet codebase:
208250

209251
## Putting It All Together
210252

211-
```js title="calculate-optimal-fee.ts" showLineNumbers mark=1-18
212-
async function calculateOptimalFee(method, relay) {
253+
```js title="calculate-optimal-fee.ts" showLineNumbers mark=1-21
254+
async function calculateOptimalFee(relay, provider, transferContract) {
213255
// 1. Get gas prices
214256
const networkGasPrice = await provider.getGasPrice()
215-
const gasPrice = getBufferedGasPrice(networkGasPrice, relay.minGasPrice, method)
257+
const baseGasPrice = networkGasPrice.gt(relay.minGasPrice)
258+
? networkGasPrice
259+
: relay.minGasPrice
260+
261+
// 2. Apply buffer
262+
const bufferedGasPrice = baseGasPrice.mul(110).div(100) // 10% mainnet
216263

217-
// 2. Get gas limit
218-
const gasLimit = GAS_LIMITS[method]
264+
// 3. Get gas limit from contract
265+
const gasLimit = await transferContract.getRequiredRelayGas('0x8d89149b')
219266

220-
// 3. Calculate chain token fee
221-
const baseCost = gasPrice.mul(gasLimit)
267+
// 4. Calculate POL fee
268+
const baseCost = bufferedGasPrice.mul(gasLimit)
222269
const withPctFee = baseCost.mul(100 + relay.pctRelayFee).div(100)
223270
const totalPOL = withPctFee.add(relay.baseRelayFee)
224271

225-
// 4. Convert to USDT with buffer
226-
const usdtFee = await convertPOLtoUSDT(totalPOL, 1.10) // 10% buffer
272+
// 5. Convert to USDT via Uniswap
273+
const polPerUsdt = await getPolUsdtPrice(provider)
274+
const usdtFee = totalPOL.mul(1_000_000).div(polPerUsdt).mul(110).div(100)
227275

228-
return { usdtFee, gasPrice, gasLimit }
276+
return { usdtFee, gasPrice: bufferedGasPrice, gasLimit }
229277
}
230278
```
231279

@@ -238,9 +286,9 @@ Reuse this helper whenever you prepare a meta-transaction so each request reflec
238286
You now have a production-grade fee engine that:
239287

240288
- ✅ Tracks live gas prices and relay minimums.
289+
- ✅ Queries contract for accurate gas limits.
290+
- ✅ Uses Uniswap V3 for real-time POL/USDT rates.
241291
- ✅ Applies thoughtful buffers to avoid underpayment.
242-
- ✅ Converts POL costs into USDT predictably.
243292
- ✅ Compares relays and selects the most cost-effective option.
244293

245-
At this point your gasless transaction pipeline matches the approach we ship in the Nimiq wallet - ready for real users. From here you can integrate oracles, caching layers, and monitoring to keep everything running smoothly.
246-
Continue with the [Nimiq Developer Center](https://developers.nimiq.com/) recipes to embed the finished fee engine in your dApp UI.
294+
At this point your gasless transaction pipeline matches the approach we ship in the Nimiq wallet - ready for real users. The next lesson covers USDC transfers using the EIP-2612 permit approval method, showing how different tokens require different approval strategies.

0 commit comments

Comments
 (0)