Skip to content

Commit d7ac4ba

Browse files
More V2 fixes (#1778)
* fix: cover L2→AH path + missing dry-run RefundSurplus + safer envName switch Three follow-ups to clara/fix-eth-dust-usdt review: 1. transfers/l2ToPolkadot/erc20ToAH.ts: fee() and tx() were untouched in the prior PR; the L2 path still trapped USDT. Add the executionFee buffer and pass userAssetLocation to sendMessageXCM. 2. xcmbuilders/toPolkadot/pnaToParachain.ts: buildParachainPNAReceivedXcmOnDestination (the V4 destination dry-run helper for PNA→parachain) was missing RefundSurplus before its DepositAsset, so the dry-run hides BuyExecution surplus dust the same way the V5 builders did before fix. 3. xcmBuilder.ts: accountToLocationWithNetwork silently returned undefined for unknown envName values (causing decode errors downstream) and was missing local_e2e in the AccountKey20 branch. Add default throw and the missing case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixes --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6f3da60 commit d7ac4ba

6 files changed

Lines changed: 52 additions & 8 deletions

File tree

web/packages/api/src/toEthereum_v2.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -915,14 +915,20 @@ export async function signAndSendTransfer(
915915
console.error(c.toHuman())
916916
reject(c.internalError || c.dispatchError || c)
917917
}
918-
// We have to check for finalization here because re-orgs will produce a different messageId on Asset Hub.
919-
// TODO: Change back to isInBlock when we switch to pallet-xcm.execute for Asset Hub and we can generate the messageId offchain.
920-
if (c.isFinalized) {
918+
// When the messageId is computed off-chain (V2 paths using polkadotXcm.execute
919+
// with a SetTopic we control), re-orgs can't change it — resolve on isInBlock to
920+
// avoid the ~12–24s finalization wait. The legacy AH path leaves messageId
921+
// undefined and must keep waiting for finality to read the event-emitted id.
922+
const resolveOnInBlock = transfer.computed.messageId !== undefined
923+
if ((resolveOnInBlock && c.isInBlock) || c.isFinalized) {
924+
const blockHash = c.isInBlock
925+
? c.status.asInBlock.toHex()
926+
: c.status.asFinalized.toHex()
921927
const result = {
922928
txHash: u8aToHex(c.txHash),
923929
txIndex: c.txIndex || 0,
924930
blockNumber: Number((c as any).blockNumber),
925-
blockHash: "",
931+
blockHash,
926932
events: c.events,
927933
}
928934
for (const e of c.events) {
@@ -954,7 +960,6 @@ export async function signAndSendTransfer(
954960
}
955961
})
956962

957-
result.blockHash = u8aToHex(await sourceParachain.rpc.chain.getBlockHash(result.blockNumber))
958963
result.messageId = transfer.computed.messageId ?? result.messageId
959964

960965
return result

web/packages/api/src/transfers/l2ToPolkadot/erc20ToAH.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import {
2626
buildAssetHubERC20ReceivedXcm,
2727
} from "../../xcmbuilders/toPolkadot/erc20ToAH"
2828
import { accountId32Location, erc20Location } from "../../xcmBuilder"
29-
import { DOT_LOCATION, ETHER_TOKEN_ADDRESS } from "../../assets_v2"
29+
import {
30+
DOT_LOCATION,
31+
ETHER_TOKEN_ADDRESS,
32+
getAssetHubEtherMinBalance,
33+
} from "../../assets_v2"
3034
import { ensureValidationSuccess, padFeeByPercentage } from "../../utils"
3135
import { resolveBeneficiary } from "../../crypto"
3236
import { FeeInfo, ValidationLog, ValidationReason } from "../../types/toPolkadot"
@@ -136,6 +140,13 @@ export class ERC20ToAH<T extends EthereumProviderTypes> implements TransferInter
136140
await assetHubImpl.swapAsset1ForAsset2(DOT_LOCATION, ether, assetHubExecutionFeeDOT),
137141
feePadPercentage ?? 33n,
138142
)
143+
// For non-ether transfers, oversize executionFee by AH bridged-ether
144+
// min_balance: the post-PayFees surplus then naturally lands at the
145+
// recipient via RefundSurplus → DepositAsset, satisfying
146+
// `Token::BelowMinimum` on a fresh asset account.
147+
if (tokenAddress !== ETHER_TOKEN_ADDRESS) {
148+
assetHubExecutionFeeEther += getAssetHubEtherMinBalance(registry)
149+
}
139150

140151
const { relayerFee, extrinsicFeeDot, extrinsicFeeEther } = await calculateRelayerFee(
141152
assetHubImpl,
@@ -361,12 +372,18 @@ export class ERC20ToAH<T extends EthereumProviderTypes> implements TransferInter
361372
accountNonce,
362373
)
363374

375+
// For ether transfers there's only one asset in holding so no split is needed.
376+
const userAssetLocation =
377+
tokenAddress === ETHER_TOKEN_ADDRESS
378+
? undefined
379+
: erc20Location(registry.ethChainId, tokenAddress)
364380
const xcm = hexToU8a(
365381
sendMessageXCM(
366382
assetHub.registry,
367383
beneficiaryAddressHex,
368384
topic,
369385
options?.customXcm,
386+
userAssetLocation,
370387
).toHex(),
371388
)
372389
let claimer = claimerFromBeneficiary(assetHub, beneficiaryAddressHex, registry.environment)

web/packages/api/src/xcmBuilder.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,8 @@ export const accountToLocationWithNetwork = (account: string, envName: string) =
14761476
}
14771477
break
14781478
}
1479+
default:
1480+
throw Error(`Unsupported envName: ${envName}`)
14791481
}
14801482
break
14811483
case 2:
@@ -1499,6 +1501,14 @@ export const accountToLocationWithNetwork = (account: string, envName: string) =
14991501
}
15001502
break
15011503
}
1504+
case "local_e2e": {
1505+
beneficiaryLocation = {
1506+
accountKey20: { key: hexAddress, network: { Polkadot: { network: null } } },
1507+
}
1508+
break
1509+
}
1510+
default:
1511+
throw Error(`Unsupported envName: ${envName}`)
15021512
}
15031513
break
15041514
default:

web/packages/api/src/xcmbuilders/toPolkadot/pnaToParachain.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ export function buildParachainPNAReceivedXcmOnDestination(
190190
],
191191
},
192192
{ clearOrigin: null },
193+
// Mirror the user-side `sendMessageXCM` remoteXcm tail: RefundSurplus
194+
// returns unused BuyExecution surplus to holding so the dry-run sees
195+
// the same dust deposit attempt that production performs.
196+
{ refundSurplus: null },
193197
...(customXcm || []), // Insert custom XCM instructions if provided
194198
...buildSplitDepositAsset(beneficiaryLocation, assetLocation, 2),
195199
{ setTopic: topic },

web/packages/operations/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
"transferWndToPenpalV2": "npx ts-node src/transfer_from_e2p_v2.ts 2000 WND 1000000000",
7777
"transferUSDCToAHV2": "npx ts-node src/transfer_from_e2p_v2.ts 1000 USDC 100000",
7878
"transferUSDTToAHV2": "npx ts-node src/transfer_from_e2p_v2.ts 1000 USDT 15000000",
79+
"trapUSDTBelowMinBalance": "SKIP_VALIDATION=true npx ts-node src/transfer_from_e2p_v2.ts 1000 USDT 5000",
80+
"claimTrap": "npx ts-node src/claim_trap.ts",
7981
"transferUSDCFromBaseToAH-sepolia": "npx ts-node src/transfer_from_l2_to_polkadot_wrapper.ts 84532 1000 USDC 1000000",
8082
"transferUSDCFromAHToBase-sepolia": "npx ts-node src/transfer_from_polkadot_to_l2_wrapper.ts 1000 84532 USDC 1000000",
8183
"transferEtherFromBaseToAH-sepolia": "npx ts-node src/transfer_from_l2_to_polkadot_wrapper.ts 84532 1000 Eth 1000000000000000",

web/packages/operations/src/transfer_to_polkadot_v2.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,15 @@ export const transferToPolkadot = async (
9696
const validation = await transferImpl.validate(transfer)
9797
console.log("validation result", validation)
9898

99-
// Step 4. Check validation logs for errors
99+
// Step 4. Check validation logs for errors. SKIP_VALIDATION=true lets
100+
// trap-test scripts proceed even when the dry-run correctly fails (e.g.
101+
// intentionally sending USDT below min_balance to trigger an asset trap).
100102
if (!validation.success) {
101-
throw Error(`validation has one of more errors.`)
103+
if (process.env.SKIP_VALIDATION === "true") {
104+
console.warn("validation failed; proceeding anyway (SKIP_VALIDATION=true)")
105+
} else {
106+
throw Error(`validation has one of more errors.`)
107+
}
102108
}
103109

104110
// Step 5. Estimate the cost of the execution cost of the transaction

0 commit comments

Comments
 (0)