diff --git a/.beads/pr-context.jsonl b/.beads/pr-context.jsonl index ec4cf00c824..e4ae58a7404 100644 --- a/.beads/pr-context.jsonl +++ b/.beads/pr-context.jsonl @@ -1,3 +1,6 @@ +{"id":"shapeshift-185","title":"Use triggerconstantcontract for dynamic Sun.io swap fee estimation","description":"## Context\n\nSun.io swap energy consumption varies dramatically by route complexity:\n- TRX→Token: 77k–336k energy (median 200k)\n- TRC20→Token: 166k–426k energy (median 303k)\n\nOur current static estimates target the median, which means simple single-hop swaps show ~2x overestimated fees while complex multi-hop routes can be underestimated. A user doing a simple TRX→USDT swap sees ~12 TRX estimated but pays ~5.5 TRX.\n\n## Research Findings\n\n### Data from 172 real Sun.io router transactions\n\n**TRX-to-Token (47 txs):**\n| Percentile | Total Energy |\n|---|---|\n| P10 | 118,575 |\n| P25 | 149,194 |\n| P50 | 199,676 |\n| P75 | 238,224 |\n| P90 | 266,769 |\n\n**TRC20-to-Token (125 txs):**\n| Percentile | Total Energy |\n|---|---|\n| P10 | 195,781 |\n| P25 | 277,344 |\n| P50 | 302,542 |\n| P75 | 320,933 |\n| P90 | 359,650 |\n\n### Key technical findings\n- TronGrid does NOT support `estimateenergy` API (returns \"this node does not support estimate energy\")\n- TronGrid DOES support `triggerconstantcontract` which simulates a contract call and returns `energy_used` + `energy_penalty`\n- This is the same approach TronLink and other TRON wallets use for fee estimation\n- The existing codebase already uses `triggerconstantcontract` in `TronApi.estimateTRC20TransferFee()` — follow that pattern\n- Sun.io router contract: `TCFNp179Lg46D16zKoumd4Poa2WFFdtqYj`\n- Contract params: `consume_user_resource_percent=60`, `origin_energy_limit=1,200,000`, `energy_factor=0`\n- Current energy price: 100 sun/unit, bandwidth price: 1000 sun/unit\n\n## Approach\n\nCall `triggerconstantcontract` on the Sun.io router with the actual swap calldata to get a per-route energy estimate. The swap route and encoded calldata are already available from the Sun.io API response (`bestRoute`). The transaction encoding logic already exists in `getUnsignedTronTransaction()` — extract or reuse the ABI encoding to build the call for simulation.\n\n### Flow:\n1. Get Sun.io quote/route (already done before fee estimation)\n2. Encode the swap call using the route data (same ABI encoding as execution)\n3. Call `triggerconstantcontract` with the encoded data to simulate execution\n4. Read `energy_used` from response, add `energy_penalty` if present\n5. Apply `consume_user_resource_percent` (60%) to get user's share\n6. Apply small safety margin (1.1x) for estimation variance\n7. Fall back to current static estimates if simulation fails\n\n### Important considerations:\n- This adds 1 RPC call per quote/rate — must be added to the existing `Promise.all` to avoid extra latency\n- Cache the result per route hash (same route = same energy) with short TTL (~30s)\n- For rates without wallet connected, we still need the calldata to simulate — check if Sun.io route data is sufficient to encode the call without a sender address (may need a dummy sender for simulation)\n- `triggerconstantcontract` does NOT consume energy or cost anything — it's a read-only simulation\n\n## Affected Files\n\n- `packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts` — main fee estimation logic, replace static energy with dynamic\n- `packages/swapper/src/swappers/SunioSwapper/endpoints.ts` — contains `getUnsignedTronTransaction()` with the ABI encoding logic to reference/extract\n- `packages/swapper/src/swappers/SunioSwapper/utils/constants.ts` — Sun.io router contract address and ABI\n- `packages/unchained-client/src/tron/api.ts` — existing `estimateTRC20TransferFee()` pattern to follow for `triggerconstantcontract` usage\n\n## Acceptance Criteria\n\n- [ ] Fee estimation uses `triggerconstantcontract` to simulate the actual swap and get real energy usage\n- [ ] Estimated fee for a simple TRX→token single-hop swap is within 20% of actual on-chain cost\n- [ ] Estimated fee for a complex multi-hop TRC20→token swap is within 20% of actual on-chain cost\n- [ ] Falls back gracefully to current static estimates if simulation fails (429, timeout, etc.)\n- [ ] No additional latency — simulation runs in parallel with other RPC calls\n- [ ] Works for both rates (no wallet) and quotes (with wallet) — use dummy sender if needed for rates\n- [ ] Passes `yarn lint --fix` and `yarn type-check`","status":"closed","priority":2,"issue_type":"feature","owner":"premiumjibles@gmail.com","created_at":"2026-02-27T15:49:58.395221393+07:00","created_by":"Jibles","updated_at":"2026-03-01T20:11:13.315653109+07:00","closed_at":"2026-03-01T20:11:13.315653109+07:00","close_reason":"Implemented dynamic fee estimation using triggerConstantContract with real sender address. Falls back to static estimates for rates."} +{"id":"shapeshift-20c","title":"Improve stemMatch validator with per-language morphology","description":"## Problem\n\nThe translation validator's `stemMatch` function in `scripts/script-utils.js` uses a **generic 70% prefix-chop algorithm** for all inflected languages. This is the single biggest source of false flags in the translation pipeline, causing:\n\n1. **False rejections** that force translators into grammatically awkward constructions to satisfy the validator\n2. **Inflated \"manual review\" counts** — UK had 19 strings flagged as manual review that were actually correct Ukrainian\n3. **Degraded translation quality** — RU agent was forced to use infinitive \"торговать\" mixed with imperatives just to pass validation\n\n### Benchmark Evidence (150 keys × 9 locales)\n\n**Glossary term match rates (skill translations):**\n- \"dust\" (never-translate): 100% — binary check works perfectly\n- \"wallet\": 72% — moderate inflection\n- \"staking\": 67% — moderate inflection \n- \"seed phrase\": 67%\n- \"swap\": 64%\n- \"claim\": **47%** — heavy inflection across cases/conjugations\n- \"deposit\": **40%** — heavy inflection, worst performer\n\nThese low rates are **not translation errors** — they're validator false positives. The translations used correct inflected forms (genitive, dative, imperative) that the naive prefix matcher can't recognize.\n\n### Root Cause\n\nTwo code paths in `stemMatch` (script-utils.js:11-24):\n\n1. **CJK (ja, zh)**: Exact substring match — works fine\n2. **Everything else**: Generic 70% prefix of each word — treats German, Russian, and Turkish identically despite fundamentally different morphology\n\n| Language | Morphology Type | What Changes | 70% Prefix Works? |\n|----------|----------------|--------------|-------------------|\n| de (Germanic) | Compounds + mild case | Articles change, stems stable | Yes, mostly fine |\n| es/fr/pt (Romance) | Verb conjugation + gender | Suffixes change | Borderline |\n| ru/uk (Slavic) | 6 cases + verb aspect | Stem + suffix both change | No — too many false flags |\n| tr (Agglutinative) | Suffix stacking + vowel harmony | Suffixes modify vowels | No — root preserved but suffixes stack deep |\n\n### Affected Files\n\n- `.claude/skills/translate/scripts/script-utils.js` — `stemMatch()` function (lines 11-24), `INFLECTED_LOCALES` set\n- `.claude/skills/translate/scripts/validate.js` — Check 5 (glossary compliance, lines 110-126) calls `stemMatch`\n- `src/assets/translations/glossary.json` — approved term definitions (currently single-form strings per locale)\n\n### Requirements\n\n1. Replace the generic stemMatch with **per-language matching strategies** that account for each language family's morphological patterns\n2. Support **multi-form glossary entries** (arrays alongside strings) for terms with irregular inflections, backward-compatible with existing string entries\n3. Per-language stem ratios: Germanic ~75%, Romance ~65%, Slavic ~55%, Agglutinative ~60%\n4. Per-language **suffix stripping**: strip known grammatical suffixes before matching to find the root (e.g., Russian case endings -а/-у/-е/-ом/-ой, Turkish agglutinative suffixes -ı/-in/-da/-lar, Romance conjugation endings -ado/-ido/-ando/-ción)\n5. Three-tier matching: exact substring → stem prefix → suffix-stripped root. Pass on first hit.\n6. Consider downgrading glossary-approved-translation from \"flag\" to \"info\" severity for inflected locales, so valid inflections don't trigger the review/refine loop\n\n### Acceptance Criteria\n\n- stemMatch has per-language configs (stem ratio + suffix list) for all 7 inflected locales\n- Glossary entries accept `string | string[]` per locale\n- Existing validate.js tests (if any) still pass\n- Re-running the benchmark shows \"claim\" and \"deposit\" match rates above 80%\n- No grammatically awkward forced forms in RU/UK/TR output","status":"closed","priority":2,"issue_type":"task","owner":"premiumjibles@gmail.com","created_at":"2026-02-24T18:38:15.541277548+07:00","created_by":"Jibles","updated_at":"2026-02-24T19:13:48.295324618+07:00","closed_at":"2026-02-24T19:13:48.295324618+07:00","close_reason":"Implemented per-language morphology for stemMatch. Replaced INFLECTED_LOCALES with LOCALE_CONFIGS (per-language stemRatio + suffix lists for de/es/fr/pt/ru/tr/uk). Added stripSuffix helper and 3-tier matching (exact→stem→suffix-stripped). Added multi-form glossary support (string|string[]) for claim/deposit/trade/approve in ru/uk/tr. Downgraded glossary flags to info severity for inflected locales. Updated prepare-locale.js for canonical form extraction and compile-report.js for clean display."} +{"id":"shapeshift-saq","title":"Fix Sun.io swap fee estimation and OUT_OF_ENERGY error handling","description":"## Problem\n\nSun.io TRX trades broadcast despite insufficient gas, fail on-chain with OUT_OF_ENERGY, and then spin as \"pending\" indefinitely in the notification center.\n\nLinear ticket: SS-5573 / GitHub: shapeshift/web#12039\n\nTwo distinct bugs:\n1. **Fee estimation is 45-90x too low** — the UI gas check passes when it shouldn't, so the trade broadcasts and fails on-chain\n2. **checkTradeStatus doesn't detect OUT_OF_ENERGY** — failed trades show as \"pending\" forever instead of showing an error\n\n## Research Findings\n\n### Root Cause 1: Hardcoded energy estimate assumes contract subsidy covers ~98% of energy\n\nThe code at \\`getQuoteOrRate.ts:159\\` hardcodes \\`energyUsed = 2000\\` with the comment \"Sun.io contract owner provides most energy (~117k), users only pay ~2k\". This is wrong.\n\n**Actual contract settings** (queried live from \\`/wallet/getcontract\\`):\n\n SmartExchangeRouter (TCFNp179Lg46D16zKoumd4Poa2WFFdtqYj):\n consume_user_resource_percent: 60 ← user pays 60%, NOT ~2%\n origin_energy_limit: 1,200,000 ← cap on contract's subsidy per call\n energy_factor: 0 ← Dynamic Energy Model not active\n\n**Actual energy usage from live successful transactions:**\n\n| Swap Type | Total Energy | Contract Paid (~40%) | User Paid (~60%) | User Fee (TRX) |\n|---------------|-------------|---------------------|-----------------|----------------|\n| TRC20 swap | 302,639 | 121,055 | 181,584 | 0-5+ TRX |\n| TRX→token | 149,194 | 59,677 | 89,517 | ~2.48 TRX |\n\nCurrent estimate: 2,000 energy = ~0.2 TRX. Actual user cost: 89,000-181,000 energy = 2.5-18 TRX.\n\n**The failed transaction from the bug report confirms this:**\n\n TX: 5660b12d88db18221865a1d480665b257d89177bde292f8b24ce4f6e73511949\n Result: OUT_OF_ENERGY\n Energy total: 10,116\n Origin energy (contract paid): 4,046 (40%)\n User energy: 6,070 (60%)\n Energy fee burned: 0.607 TRX\n Total fee lost: 1.534 TRX\n\n### Root Cause 2: checkTradeStatus only checks for REVERT, not OUT_OF_ENERGY\n\nAt \\`endpoints.ts:188-190\\`, the status check maps \\`REVERT\\` → \\`TxStatus.Failed\\` but \\`OUT_OF_ENERGY\\` falls through to \\`TxStatus.Pending\\`. Other parts of the codebase handle this correctly:\n- \\`src/lib/utils/tron.ts:46\\` — checks for OUT_OF_ENERGY → Failed\n- \\`useAllowanceApproval.tsx:146\\` — checks for REVERT || OUT_OF_ENERGY\n\n### Approaches Considered and Ruled Out\n\n**1. Use Sun.io API fee data** — RULED OUT\nThe Sun.io quote API (\\`/swap/router\\`) returns only routing/pricing data. No energy, gas, bandwidth, or network fee fields exist in the response. The \\`fee\\` field is the protocol swap fee (e.g. 3%), not a network fee. Confirmed by hitting the live API.\n\n**2. Use triggerConstantContract to simulate the swap** — RULED OUT\nWorks for simple \\`transfer(address,uint256)\\` calls (used by our existing \\`estimateTRC20TransferFee\\`), but Sun.io's \\`swapExactInput\\` reverts in simulation because the from address doesn't have token approvals/balances. Swap functions have preconditions that prevent dry-run simulation.\n\n**3. Use estimateEnergy API** — RULED OUT (for now)\nTronGrid (our TRON RPC) has this endpoint disabled (\\`\"this node does not support estimate energy\"\\`). Would require switching to a paid TRON RPC provider like TronQL. Not worth the infrastructure change for this fix.\n\n**4. Method 3 from TRON docs (max_factor)** — RULED OUT\nmax_factor is 3.4x on mainnet. Applied naively it would show 39-80 TRX fees for swaps that actually cost 2.5-18 TRX. This over-penalizes Sun.io in the UI, making it look uncompetitive.\n\n### Chosen Approach: Empirical estimates + live contract parameter query\n\nSince Sun.io's \\`energy_factor = 0\\` (Dynamic Energy Model doesn't apply — usage is 0.008% of the 5B threshold), the energy costs are stable and predictable. We can:\n\n1. Use realistic base energy values derived from live transaction data\n2. Query \\`consume_user_resource_percent\\` and \\`origin_energy_limit\\` from the contract to dynamically calculate the user's share\n3. This auto-adapts if Sun.io changes their contract settings without needing code changes\n\n**Energy formula:**\n adjustedEnergy = baseEnergy × (1 + energy_factor)\n contractShare = min(adjustedEnergy × (1 - consumeUserPercent/100), originEnergyLimit)\n userEnergy = adjustedEnergy - contractShare\n energyFee = userEnergy × energyPrice\n\n**Base energy values (from live txs, apply 1.3x safety margin):**\n- TRX→token swaps: ~150,000 total energy → use 195,000\n- TRC20→token swaps: ~300,000 total energy → use 390,000\n\n## Implementation Spec\n\n### Change 1: Fix fee estimation in getQuoteOrRate.ts\n\nFile: \\`packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts\\` (lines 117-199)\n\nReplace the hardcoded \\`energyUsed = 2000\\` with realistic estimates:\n\n- Query the Sun.io contract's \\`consume_user_resource_percent\\` and \\`origin_energy_limit\\` via \\`/wallet/getcontract\\` (cache this — it changes rarely)\n- Also query \\`energy_factor\\` via \\`/wallet/getcontractinfo\\` (cache alongside)\n- Use base energy of ~150,000 for TRX swaps, ~300,000 for TRC20 swaps (with 1.3x safety margin)\n- Calculate user's share: \\`totalEnergy × consumeUserPercent/100\\`\n- Multiply by live \\`energyPrice\\` (already fetched from chain params)\n- The bandwidth estimate of 1100 bytes is roughly correct based on actual tx sizes seen (~927-1116 bytes) — keep it\n\nConsider caching the contract params in a module-level variable with a TTL (e.g. 1 hour), since they change rarely and we don't want an extra RPC call per quote.\n\n### Change 2: Fix checkTradeStatus in endpoints.ts\n\nFile: \\`packages/swapper/src/swappers/SunioSwapper/endpoints.ts\\` (lines 185-190)\n\nAdd \\`OUT_OF_ENERGY\\` as a failure condition. Current code:\n\n contractRet === 'REVERT' ? TxStatus.Failed : TxStatus.Pending\n\nShould be:\n\n contractRet === 'REVERT' || contractRet === 'OUT_OF_ENERGY' ? TxStatus.Failed : TxStatus.Pending\n\nThis matches the pattern used in \\`src/lib/utils/tron.ts:46\\` and \\`useAllowanceApproval.tsx:146\\`.\n\nAlso consider handling other TRON failure codes: \\`OUT_OF_TIME\\`, \\`JVM_STACK_OVER_FLOW\\`, \\`UNKNOWN\\`, \\`TRANSFER_FAILED\\`, etc. A safe approach: treat any \\`contractRet\\` that isn't \\`SUCCESS\\` and isn't absent/null as Failed (with the exception of no contractRet yet = still pending).\n\n### Change 3 (optional): Also check for OUT_OF_ENERGY in other Tron swappers\n\nCheck if Relay and ButterSwap's checkTradeStatus implementations have the same gap. If they delegate to a shared utility, this may already be covered. If they have their own Tron status checking, apply the same fix.\n\n## Relevant Files\n\n**Primary (must change):**\n- \\`packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts\\` — fee estimation (lines 117-199)\n- \\`packages/swapper/src/swappers/SunioSwapper/endpoints.ts\\` — checkTradeStatus (lines 185-190)\n\n**Reference (correct patterns to follow):**\n- \\`src/lib/utils/tron.ts:46\\` — correct OUT_OF_ENERGY handling\n- \\`src/components/MultiHopTrade/components/TradeConfirm/hooks/useAllowanceApproval.tsx:146\\` — correct OUT_OF_ENERGY handling\n- \\`packages/unchained-client/src/tron/api.ts:248-277\\` — estimateTRC20TransferFee pattern (for reference on how energy estimation is done for simpler calls)\n- \\`packages/chain-adapters/src/tron/TronChainAdapter.ts:457-573\\` — getFeeData (reference for energy calculation with safety margins)\n\n**Context (read for understanding):**\n- \\`packages/swapper/src/swappers/SunioSwapper/types.ts\\` — SunioRoute type (no fee fields from API)\n- \\`packages/swapper/src/swappers/SunioSwapper/utils/constants.ts\\` — contract address, API URL\n- \\`packages/swapper/src/swappers/SunioSwapper/INTEGRATION.md\\` — integration docs\n- \\`src/state/apis/swapper/helpers/validateTradeQuote.ts:211-223\\` — the balance check that uses the fee estimate\n\n**Key external data:**\n- Sun.io SmartExchangeRouter contract: \\`TCFNp179Lg46D16zKoumd4Poa2WFFdtqYj\\`\n- consume_user_resource_percent: 60\n- origin_energy_limit: 1,200,000\n- energy_factor: 0\n- Current energyPrice: 100 sun/unit\n- Current bandwidthPrice: 1,000 sun/byte\n\n## Acceptance Criteria\n\n1. Trade does not broadcast if user has insufficient TRX to cover realistic gas estimate\n2. Fee displayed in UI is within ~1.5x of actual on-chain cost (not 45-90x too low)\n3. If a trade fails on-chain with OUT_OF_ENERGY, the UI shows it as failed (not stuck on \"pending\")\n4. Fee estimation adapts if Sun.io changes contract energy settings (consume_user_resource_percent, origin_energy_limit) without code changes\n5. Existing lint and type-check pass (\\`yarn lint --fix \u0026\u0026 yarn type-check\\`)","status":"closed","priority":1,"issue_type":"bug","owner":"premiumjibles@gmail.com","created_at":"2026-02-27T10:09:30.218673815+07:00","created_by":"Jibles","updated_at":"2026-02-27T10:20:04.19372967+07:00","closed_at":"2026-02-27T10:20:04.19372967+07:00","close_reason":"Implemented dynamic fee estimation using contract params + 1-hour cache, fixed checkTradeStatus in Sun.io and ButterSwap to catch all non-SUCCESS failure states. PR #12045."} {"id":"shapeshiftWeb-2f09","title":"Sanity check Ink + Scroll regen data","description":"Verify generatedAssetData.json has entries for both eip155:534352 (Scroll) and eip155:57073 (Ink). Verify relatedAssetIndex.json has inkAssetId in ETH related array. Verify no regressions. Run review-second-class-evm skill.","status":"closed","priority":1,"issue_type":"task","owner":"contact@0xgom.es","created_at":"2026-02-19T13:51:24.013329+01:00","created_by":"gomes-bot","updated_at":"2026-02-19T17:01:37.198079+01:00","closed_at":"2026-02-19T17:01:37.198079+01:00","close_reason":"Popular assets + market data verified working after cache clear. All ink fixes merged, PR #11960 opened.","dependencies":[{"issue_id":"shapeshiftWeb-2f09","depends_on_id":"shapeshiftWeb-cgtg","type":"blocks","created_at":"2026-02-19T13:51:48.716437+01:00","created_by":"gomes-bot"}]} {"id":"shapeshiftWeb-4uq9","title":"Checkout + merge-fix Ink PR #11904","description":"Checkout Ink PR, extract regen data before merge, merge origin/develop with -X theirs to resolve all conflicts in favor of develop. Result: branch has Ink code changes but develop's generated files.","status":"closed","priority":1,"issue_type":"task","owner":"contact@0xgom.es","created_at":"2026-02-19T13:50:52.705351+01:00","created_by":"gomes-bot","updated_at":"2026-02-19T13:53:13.843624+01:00","closed_at":"2026-02-19T13:53:13.843624+01:00","close_reason":"Merged origin/develop with -X theirs, all conflicts resolved"} {"id":"shapeshiftWeb-cgtg","title":"Cherry-pick Ink regen data into develop generated files","description":"Extract Ink (eip155:57073) entries from saved PR generated files. Merge into develop's generatedAssetData.json, relatedAssetIndex.json. Create coingecko adapter. Bump clearAssets migration. Regenerate manifest hashes + brotli/gzip compression.","status":"closed","priority":1,"issue_type":"task","owner":"contact@0xgom.es","created_at":"2026-02-19T13:51:03.136273+01:00","created_by":"gomes-bot","updated_at":"2026-02-19T13:56:30.878339+01:00","closed_at":"2026-02-19T13:56:30.878339+01:00","close_reason":"Added coingecko adapter, index.ts import/export, migration bump 293. User will run yarn generate:asset-data for actual regen.","dependencies":[{"issue_id":"shapeshiftWeb-cgtg","depends_on_id":"shapeshiftWeb-4uq9","type":"blocks","created_at":"2026-02-19T13:51:38.351227+01:00","created_by":"gomes-bot"}]} diff --git a/.env b/.env index 2f73c970f40..3d8696551dd 100644 --- a/.env +++ b/.env @@ -182,6 +182,7 @@ VITE_MAYACHAIN_NODE_URL=https://api.mayachain.shapeshift.com/lcd VITE_SOLANA_NODE_URL=https://api.solana.shapeshift.com/api/v1/jsonrpc VITE_STARKNET_NODE_URL=https://rpc.starknet.lava.build VITE_TRON_NODE_URL=https://api.trongrid.io +VITE_TRON_ESTIMATE_ENERGY_URL=https://tron-mainnet.core.chainstack.com/49869cfc535bb27e2803c06fabe3e74a VITE_NEAR_NODE_URL=https://rpc.mainnet.near.org VITE_NEAR_NODE_URL_FALLBACK_1=https://near.lava.build VITE_NEAR_NODE_URL_FALLBACK_2=https://rpc.fastnear.com diff --git a/.env.development b/.env.development index feb3a8b9793..1e8b5592692 100644 --- a/.env.development +++ b/.env.development @@ -91,6 +91,7 @@ VITE_MAYACHAIN_NODE_URL=https://dev-api.mayachain.shapeshift.com/lcd VITE_SOLANA_NODE_URL=https://dev-api.solana.shapeshift.com/api/v1/jsonrpc VITE_STARKNET_NODE_URL=https://rpc.starknet.lava.build VITE_TRON_NODE_URL=https://api.trongrid.io +VITE_TRON_ESTIMATE_ENERGY_URL=https://tron-mainnet.core.chainstack.com/49869cfc535bb27e2803c06fabe3e74a # midgard VITE_THORCHAIN_MIDGARD_URL=https://dev-api.thorchain.shapeshift.com/midgard/v2 diff --git a/headers/csps/chains/tron.ts b/headers/csps/chains/tron.ts index 0a593eb3cdb..672477fbda0 100644 --- a/headers/csps/chains/tron.ts +++ b/headers/csps/chains/tron.ts @@ -6,5 +6,5 @@ const mode = process.env.MODE ?? process.env.NODE_ENV ?? 'development' const env = loadEnv(mode, process.cwd(), '') export const csp: Csp = { - 'connect-src': [env.VITE_TRON_NODE_URL], + 'connect-src': [env.VITE_TRON_NODE_URL, env.VITE_TRON_ESTIMATE_ENERGY_URL].filter(Boolean), } diff --git a/packages/public-api/src/config.ts b/packages/public-api/src/config.ts index a9fbdc19414..39696e69683 100644 --- a/packages/public-api/src/config.ts +++ b/packages/public-api/src/config.ts @@ -11,6 +11,7 @@ export const getServerConfig = (): SwapperConfig => ({ VITE_THORCHAIN_NODE_URL: process.env.THORCHAIN_NODE_URL || 'https://thornode.ninerealms.com', VITE_MAYACHAIN_NODE_URL: process.env.MAYACHAIN_NODE_URL || 'https://tendermint.mayachain.info', VITE_TRON_NODE_URL: process.env.TRON_NODE_URL || 'https://api.trongrid.io', + VITE_TRON_ESTIMATE_ENERGY_URL: process.env.TRON_ESTIMATE_ENERGY_URL || '', VITE_FEATURE_THORCHAINSWAP_LONGTAIL: process.env.FEATURE_THORCHAINSWAP_LONGTAIL === 'true', VITE_FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL: process.env.FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL === 'true', diff --git a/packages/swapper/src/swappers/ButterSwap/swapperApi/checkTradeStatus.ts b/packages/swapper/src/swappers/ButterSwap/swapperApi/checkTradeStatus.ts index 659b0cf654d..d0a4e0d7e7c 100644 --- a/packages/swapper/src/swappers/ButterSwap/swapperApi/checkTradeStatus.ts +++ b/packages/swapper/src/swappers/ButterSwap/swapperApi/checkTradeStatus.ts @@ -62,11 +62,10 @@ export const checkTradeStatus = async (input: CheckTradeStatusInput): Promise 0 ? TxStatus.Confirmed - : contractRet === 'REVERT' + : contractRet && contractRet !== 'SUCCESS' ? TxStatus.Failed : TxStatus.Pending diff --git a/packages/swapper/src/swappers/SunioSwapper/endpoints.ts b/packages/swapper/src/swappers/SunioSwapper/endpoints.ts index fc73d55f543..3121272ab57 100644 --- a/packages/swapper/src/swappers/SunioSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/SunioSwapper/endpoints.ts @@ -24,22 +24,10 @@ import { getSunioTradeQuote } from './getSunioTradeQuote/getSunioTradeQuote' import { getSunioTradeRate } from './getSunioTradeRate/getSunioTradeRate' import { buildSwapRouteParameters } from './utils/buildSwapRouteParameters' import { SUNIO_SMART_ROUTER_CONTRACT } from './utils/constants' +import { convertAddressesToEvmFormat } from './utils/convertAddressesToEvmFormat' const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) -const convertAddressesToEvmFormat = (value: unknown): unknown => { - if (Array.isArray(value)) { - return value.map(v => convertAddressesToEvmFormat(v)) - } - - if (typeof value === 'string' && value.startsWith('T') && TronWeb.isAddress(value)) { - const hex = TronWeb.address.toHex(value) - return hex.replace(/^41/, '0x') - } - - return value -} - export const sunioApi: SwapperApi = { getTradeQuote: async ( input: GetTronTradeQuoteInput | CommonTradeQuoteInput, @@ -181,11 +169,10 @@ export const sunioApi: SwapperApi = { const contractRet = tx.ret?.[0]?.contractRet - // Only mark as confirmed if SUCCESS AND has confirmations (in a block) const status = contractRet === 'SUCCESS' && tx.confirmations > 0 ? TxStatus.Confirmed - : contractRet === 'REVERT' + : contractRet && contractRet !== 'SUCCESS' ? TxStatus.Failed : TxStatus.Pending diff --git a/packages/swapper/src/swappers/SunioSwapper/utils/convertAddressesToEvmFormat.ts b/packages/swapper/src/swappers/SunioSwapper/utils/convertAddressesToEvmFormat.ts new file mode 100644 index 00000000000..72bbdc241fc --- /dev/null +++ b/packages/swapper/src/swappers/SunioSwapper/utils/convertAddressesToEvmFormat.ts @@ -0,0 +1,14 @@ +import { TronWeb } from 'tronweb' + +export const convertAddressesToEvmFormat = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map(v => convertAddressesToEvmFormat(v)) + } + + if (typeof value === 'string' && value.startsWith('T') && TronWeb.isAddress(value)) { + const hex = TronWeb.address.toHex(value) + return hex.replace(/^41/, '0x') + } + + return value +} diff --git a/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts b/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts index d6ae1564f95..d60554129ee 100644 --- a/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts +++ b/packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts @@ -15,11 +15,75 @@ import type { } from '../../../types' import { SwapperName, TradeQuoteError } from '../../../types' import { getInputOutputRate, makeSwapErrorRight } from '../../../utils' +import type { SunioRoute } from '../types' import { DEFAULT_SLIPPAGE_PERCENTAGE, SUNIO_SMART_ROUTER_CONTRACT } from './constants' +import { convertAddressesToEvmFormat } from './convertAddressesToEvmFormat' import { fetchSunioQuote } from './fetchFromSunio' import { isSupportedChainId } from './helpers/helpers' import { sunioServiceFactory } from './sunioService' +const ENERGY_PRICE = 100 +const USER_ENERGY_SHARE = 0.6 +const BASE_ENERGY_PER_HOP = 65_000 +const MAX_PENALTY_MULTIPLIER = 4.4 + +const estimateSwapEnergy = async ( + route: SunioRoute, + sellAmountCryptoBaseUnit: string, + senderAddress: string, + isSellingNativeTrx: boolean, + estimateEnergyUrl: string, +): Promise => { + try { + const tronWeb = new TronWeb({ fullHost: estimateEnergyUrl }) + + const path = route.tokens + const poolVersion = route.poolVersions + const versionLen = Array(poolVersion.length).fill(2) + const fees = route.poolFees.map(fee => Number(fee)) + + const swapData = { + amountIn: sellAmountCryptoBaseUnit, + amountOutMin: '0', + recipient: senderAddress, + deadline: Math.floor(Date.now() / 1000) + 60 * 20, + } + + const parameters = [ + { type: 'address[]', value: path }, + { type: 'string[]', value: poolVersion }, + { type: 'uint256[]', value: versionLen }, + { type: 'uint24[]', value: fees }, + { + type: 'tuple(uint256,uint256,address,uint256)', + value: convertAddressesToEvmFormat([ + swapData.amountIn, + swapData.amountOutMin, + swapData.recipient, + swapData.deadline, + ]), + }, + ] + + const functionSelector = + 'swapExactInput(address[],string[],uint256[],uint24[],(uint256,uint256,address,uint256))' + + const result = await tronWeb.transactionBuilder.estimateEnergy( + SUNIO_SMART_ROUTER_CONTRACT, + functionSelector, + { callValue: isSellingNativeTrx ? Number(sellAmountCryptoBaseUnit) : 0 }, + parameters, + senderAddress, + ) + + if (!result?.energy_required) return undefined + + return result.energy_required + } catch { + return undefined + } +} + export async function getQuoteOrRate( input: GetTronTradeQuoteInput | CommonTradeQuoteInput, deps: SwapperDeps, @@ -39,14 +103,13 @@ export async function getQuoteOrRate( sellAsset, buyAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit, + sendAddress, receiveAddress, accountNumber, affiliateBps, slippageTolerancePercentageDecimal, } = input - const { assertGetTronChainAdapter: _assertGetTronChainAdapter } = deps - if (!isSupportedChainId(sellAsset.chainId)) { return Err( makeSwapErrorRight({ @@ -104,7 +167,6 @@ export async function getQuoteOrRate( const isQuote = input.quoteOrRate === 'quote' - // For quotes, receiveAddress is required if (isQuote && !receiveAddress) { return Err( makeSwapErrorRight({ @@ -114,96 +176,55 @@ export async function getQuoteOrRate( ) } - // Fetch network fees for both quotes and rates (when wallet connected) - let networkFeeCryptoBaseUnit: string | undefined = undefined - - // Estimate fees when we have an address to estimate from - if (receiveAddress) { - try { - const contractAddress = contractAddressOrUndefined(sellAsset.assetId) - const isSellingNativeTrx = !contractAddress - - const tronWeb = new TronWeb({ fullHost: deps.config.VITE_TRON_NODE_URL }) - - // Get chain parameters for pricing - const params = await tronWeb.trx.getChainParameters() - const bandwidthPrice = params.find(p => p.key === 'getTransactionFee')?.value ?? 1000 - const energyPrice = params.find(p => p.key === 'getEnergyFee')?.value ?? 100 - - // Check if recipient needs activation (applies to all swaps) - let accountActivationFee = 0 - try { - const recipientInfoResponse = await fetch( - `${deps.config.VITE_TRON_NODE_URL}/wallet/getaccount`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ address: receiveAddress, visible: true }), - }, - ) - const recipientInfo = await recipientInfoResponse.json() - const recipientExists = recipientInfo && Object.keys(recipientInfo).length > 1 - if (!recipientExists) { - accountActivationFee = 1_000_000 // 1 TRX - } - } catch { - // Ignore activation check errors - } - - // For native TRX swaps, Sun.io uses a contract call with value - // We need to estimate energy for the swap contract, not just bandwidth - if (isSellingNativeTrx) { - try { - // Sun.io contract owner provides most energy (~117k), users only pay ~2k - // Use fixed 2k energy estimate instead of querying (which returns total 120k) - const energyUsed = 2000 // User pays ~2k energy, contract covers the rest - const energyFee = energyUsed * energyPrice // No multiplier - contract provides energy - - // Estimate bandwidth for contract call (much larger than simple transfer) - const bandwidthFee = 1100 * bandwidthPrice // ~1100 bytes for contract call (with safety buffer) - - networkFeeCryptoBaseUnit = bn(energyFee) - .plus(bandwidthFee) - .plus(accountActivationFee) - .toFixed(0) - } catch (estimationError) { - // Fallback estimate: ~2k energy + ~1100 bytes bandwidth + activation fee - const fallbackEnergyFee = 2000 * energyPrice - const fallbackBandwidthFee = 1100 * bandwidthPrice - networkFeeCryptoBaseUnit = bn(fallbackEnergyFee) - .plus(fallbackBandwidthFee) - .plus(accountActivationFee) - .toFixed(0) - } - } else { - // For TRC-20 swaps through Sun.io router - // Same as TRX: contract owner provides most energy, user pays ~2k - // Sun.io provides ~217k energy, user pays ~2k - const energyFee = 2000 * energyPrice - const bandwidthFee = 1100 * bandwidthPrice - - networkFeeCryptoBaseUnit = bn(energyFee) - .plus(bandwidthFee) - .plus(accountActivationFee) - .toFixed(0) - } - } catch (error) { - // For rates, fall back to '0' on estimation failure - // For quotes, let it error (required for accurate swap) - if (!isQuote) { - networkFeeCryptoBaseUnit = '0' - } else { - throw error - } - } - } - const buyAmountCryptoBaseUnit = BigAmount.fromPrecision({ value: bestRoute.amountOut, precision: buyAsset.precision, }).toBaseUnit() - // Calculate protocol fees only for quotes + const contractAddress = contractAddressOrUndefined(sellAsset.assetId) + const isSellingNativeTrx = !contractAddress + const hopCount = bestRoute.tokens.length - 1 + const rpcUrl = deps.config.VITE_TRON_NODE_URL + const estimateEnergyUrl = deps.config.VITE_TRON_ESTIMATE_ENERGY_URL + + const estimateEnergyPromise = + sendAddress && estimateEnergyUrl + ? estimateSwapEnergy( + bestRoute, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + sendAddress, + isSellingNativeTrx, + estimateEnergyUrl, + ) + : Promise.resolve(undefined) + + const accountActivationPromise = receiveAddress + ? fetch(`${rpcUrl}/wallet/getaccount`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ address: receiveAddress, visible: true }), + }) + .then(res => res.json()) + .then(info => (info && Object.keys(info).length > 1 ? 0 : 1_000_000)) + .catch(() => 0) + : Promise.resolve(0) + + const [estimatedEnergy, accountActivationFee] = await Promise.all([ + estimateEnergyPromise, + accountActivationPromise, + ]) + + const totalEnergy = + estimatedEnergy ?? Math.ceil(BASE_ENERGY_PER_HOP * hopCount * MAX_PENALTY_MULTIPLIER) + const userEnergy = totalEnergy * USER_ENERGY_SHARE + const energyFee = userEnergy * ENERGY_PRICE + const bandwidthFee = 1_100_000 + + const networkFeeCryptoBaseUnit = bn(energyFee) + .plus(bandwidthFee) + .plus(accountActivationFee) + .toFixed(0) + const protocolFeeCryptoBaseUnit = isQuote ? bn(bestRoute.fee).times(sellAmountIncludingProtocolFeesCryptoBaseUnit).toFixed(0) : '0' diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 582d4e1456a..95b565b14e4 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -57,6 +57,7 @@ export type SwapperConfig = { VITE_THORCHAIN_NODE_URL: string VITE_MAYACHAIN_NODE_URL: string VITE_TRON_NODE_URL: string + VITE_TRON_ESTIMATE_ENERGY_URL: string VITE_FEATURE_THORCHAINSWAP_LONGTAIL: boolean VITE_FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL: boolean VITE_THORCHAIN_MIDGARD_URL: string diff --git a/src/config.ts b/src/config.ts index 9c705672a1c..62bf0ef4b67 100644 --- a/src/config.ts +++ b/src/config.ts @@ -88,6 +88,7 @@ const validators = { VITE_SOLANA_NODE_URL: url(), VITE_STARKNET_NODE_URL: url(), VITE_TRON_NODE_URL: url(), + VITE_TRON_ESTIMATE_ENERGY_URL: url({ default: '' }), VITE_SUI_NODE_URL: url(), VITE_TON_NODE_URL: url(), VITE_NEAR_NODE_URL: url(),