four-flap-meme-sdk 2.2.7 → 2.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +6 -41
- package/README.md +0 -31
- package/README.zh-CN.md +6 -41
- package/dist/chains/bsc/four/approve-tokenmanager.d.ts +26 -1
- package/dist/chains/bsc/four/approve-tokenmanager.js +113 -1
- package/dist/chains/bsc/four/config.d.ts +67 -5
- package/dist/chains/bsc/four/config.js +114 -2
- package/dist/chains/bsc/four/core.d.ts +4 -1
- package/dist/chains/bsc/four/core.js +592 -1
- package/dist/chains/bsc/four/index.d.ts +6 -6
- package/dist/chains/bsc/four/index.js +6 -6
- package/dist/chains/bsc/four/internal.d.ts +46 -2
- package/dist/chains/bsc/four/internal.js +239 -2
- package/dist/chains/bsc/four/pancake-proxy.d.ts +28 -1
- package/dist/chains/bsc/four/pancake-proxy.js +687 -1
- package/dist/chains/bsc/four/private.d.ts +27 -1
- package/dist/chains/bsc/four/private.js +477 -1
- package/dist/chains/bsc/four/submit.d.ts +315 -2
- package/dist/chains/bsc/four/submit.js +752 -2
- package/dist/chains/bsc/four/swap-buy-first.d.ts +55 -2
- package/dist/chains/bsc/four/swap-buy-first.js +507 -2
- package/dist/chains/bsc/four/swap-internal.d.ts +3 -1
- package/dist/chains/bsc/four/swap-internal.js +18 -1
- package/dist/chains/bsc/four/swap.d.ts +144 -2
- package/dist/chains/bsc/four/swap.js +766 -2
- package/dist/chains/bsc/four/types.d.ts +476 -1
- package/dist/chains/bsc/four/utils.d.ts +18 -5
- package/dist/chains/bsc/four/utils.js +1552 -5
- package/dist/chains/bsc/pancake/bundle-buy-first.d.ts +91 -1
- package/dist/chains/bsc/pancake/bundle-buy-first.js +212 -97
- package/dist/chains/bsc/pancake/bundle-swap.d.ts +79 -1
- package/dist/chains/bsc/pancake/bundle-swap.js +726 -114
- package/dist/chains/bsc/pancake/index.d.ts +2 -4
- package/dist/chains/bsc/pancake/index.js +3 -1
- package/dist/chains/bsc/platforms/iro/factory.d.ts +2 -2
- package/dist/chains/bsc/platforms/iro/factory.js +1 -3
- package/dist/chains/bsc/platforms/iro/index.d.ts +5 -5
- package/dist/chains/bsc/platforms/iro/index.js +3 -3
- package/dist/chains/bsc/platforms/iro/pool.js +10 -31
- package/dist/chains/bsc/platforms/iro/token.js +1 -4
- package/dist/chains/eni/batch-router/bundle-approve.js +3 -4
- package/dist/chains/eni/batch-router/transfer.js +25 -55
- package/dist/chains/eni/batch-router/utils.js +6 -32
- package/dist/chains/eni/bundler/sign.js +6 -5
- package/dist/chains/eni/bundler/submit.js +4 -1
- package/dist/chains/eni/constants.js +1 -1
- package/dist/chains/eni/index.d.ts +1 -2
- package/dist/chains/eni/index.js +0 -1
- package/dist/chains/eni/platforms/daoaas/create.js +2 -2
- package/dist/chains/eni/platforms/daoaas/index.d.ts +3 -3
- package/dist/chains/eni/platforms/daoaas/index.js +3 -3
- package/dist/chains/eni/platforms/daoaas/meta.js +6 -9
- package/dist/chains/eni/platforms/daoaas/portal-direct.js +44 -28
- package/dist/chains/eni/platforms/daoaas/portal.js +6 -10
- package/dist/chains/eni/platforms/dswap/liquidity.js +26 -58
- package/dist/chains/eni/platforms/fair-launch/index.d.ts +2 -2
- package/dist/chains/eni/platforms/fair-launch/index.js +1 -1
- package/dist/chains/eni/platforms/fair-launch/launcher.js +46 -87
- package/dist/chains/eni/platforms/fair-launch/pool.js +1 -4
- package/dist/chains/eni/platforms/fair-launch/presets.js +2 -2
- package/dist/chains/eni/platforms/iro/factory.d.ts +2 -2
- package/dist/chains/eni/platforms/iro/factory.js +1 -3
- package/dist/chains/eni/platforms/iro/index.d.ts +6 -6
- package/dist/chains/eni/platforms/iro/index.js +4 -4
- package/dist/chains/eni/platforms/iro/pool.js +26 -90
- package/dist/chains/eni/platforms/iro/token.js +31 -107
- package/dist/chains/eni/platforms/iro/whitelist.js +18 -6
- package/dist/chains/index.d.ts +0 -13
- package/dist/chains/index.js +0 -13
- package/dist/chains/xlayer/eip7702/bundle-approve.d.ts +26 -2
- package/dist/chains/xlayer/eip7702/bundle-approve.js +21 -11
- package/dist/chains/xlayer/eip7702/bundle-buy.d.ts +6 -2
- package/dist/chains/xlayer/eip7702/bundle-buy.js +51 -13
- package/dist/chains/xlayer/eip7702/bundle-create.js +59 -93
- package/dist/chains/xlayer/eip7702/bundle-sell.d.ts +6 -2
- package/dist/chains/xlayer/eip7702/bundle-sell.js +111 -29
- package/dist/chains/xlayer/eip7702/bundle-swap.d.ts +65 -3
- package/dist/chains/xlayer/eip7702/bundle-swap.js +245 -51
- package/dist/chains/xlayer/eip7702/constants.d.ts +16 -1
- package/dist/chains/xlayer/eip7702/constants.js +21 -3
- package/dist/chains/xlayer/eip7702/index.d.ts +46 -28
- package/dist/chains/xlayer/eip7702/index.js +81 -28
- package/dist/chains/xlayer/eip7702/multi-hop-transfer.d.ts +203 -2
- package/dist/chains/xlayer/eip7702/multi-hop-transfer.js +307 -63
- package/dist/chains/xlayer/eip7702/types.d.ts +0 -88
- package/dist/chains/xlayer/eip7702/utils.d.ts +3 -0
- package/dist/chains/xlayer/eip7702/utils.js +28 -23
- package/dist/chains/xlayer/eip7702/volume.d.ts +184 -6
- package/dist/chains/xlayer/eip7702/volume.js +164 -89
- package/dist/chains/xlayer/eoa/constants.js +1 -1
- package/dist/chains/xlayer/eoa/dex-helpers.js +5 -5
- package/dist/chains/xlayer/eoa/eoa-bundle-swap.d.ts +95 -1
- package/dist/chains/xlayer/eoa/eoa-bundle-swap.js +299 -66
- package/dist/chains/xlayer/eoa/eoa-wash-volume.d.ts +1 -1
- package/dist/chains/xlayer/eoa/eoa-wash-volume.js +23 -18
- package/dist/chains/xlayer/eoa/index.d.ts +6 -10
- package/dist/chains/xlayer/eoa/index.js +23 -8
- package/dist/chains/xlayer/eoa/portal-ops.js +2 -7
- package/dist/chains/xlayer/eoa/router-manager.js +3 -3
- package/dist/chains/xlayer/eoa/types.d.ts +2 -2
- package/dist/chains/xlayer/eoa/types.js +3 -1
- package/dist/chains/xlayer/index.d.ts +2 -3
- package/dist/chains/xlayer/index.js +7 -4
- package/dist/contracts/helper3.d.ts +5 -20
- package/dist/contracts/helper3.js +20 -56
- package/dist/contracts/tm-bundle-merkle/approve-tokenmanager.d.ts +26 -1
- package/dist/contracts/tm-bundle-merkle/approve-tokenmanager.js +113 -1
- package/dist/contracts/tm-bundle-merkle/config.d.ts +67 -5
- package/dist/contracts/tm-bundle-merkle/config.js +114 -2
- package/dist/contracts/tm-bundle-merkle/core.d.ts +4 -1
- package/dist/contracts/tm-bundle-merkle/core.js +591 -1
- package/dist/contracts/tm-bundle-merkle/index.d.ts +5 -5
- package/dist/contracts/tm-bundle-merkle/index.js +5 -5
- package/dist/contracts/tm-bundle-merkle/internal.d.ts +46 -2
- package/dist/contracts/tm-bundle-merkle/internal.js +238 -2
- package/dist/contracts/tm-bundle-merkle/pancake-proxy.d.ts +28 -1
- package/dist/contracts/tm-bundle-merkle/pancake-proxy.js +686 -1
- package/dist/contracts/tm-bundle-merkle/private.d.ts +27 -1
- package/dist/contracts/tm-bundle-merkle/private.js +476 -1
- package/dist/contracts/tm-bundle-merkle/submit.d.ts +314 -3
- package/dist/contracts/tm-bundle-merkle/submit.js +928 -3
- package/dist/contracts/tm-bundle-merkle/swap-buy-first.d.ts +55 -2
- package/dist/contracts/tm-bundle-merkle/swap-buy-first.js +506 -2
- package/dist/contracts/tm-bundle-merkle/swap-internal.d.ts +3 -1
- package/dist/contracts/tm-bundle-merkle/swap-internal.js +18 -1
- package/dist/contracts/tm-bundle-merkle/swap.d.ts +144 -2
- package/dist/contracts/tm-bundle-merkle/swap.js +764 -2
- package/dist/contracts/tm-bundle-merkle/types.d.ts +476 -1
- package/dist/contracts/tm-bundle-merkle/utils.d.ts +18 -6
- package/dist/contracts/tm-bundle-merkle/utils.js +1501 -6
- package/dist/contracts/tm-bundle.d.ts +51 -3
- package/dist/contracts/tm-bundle.js +177 -108
- package/dist/contracts/tm.d.ts +2 -3
- package/dist/contracts/tm.js +32 -37
- package/dist/contracts/tm1.js +4 -9
- package/dist/contracts/tm2.js +4 -9
- package/dist/dex/direct-router.d.ts +125 -3
- package/dist/dex/direct-router.js +666 -237
- package/dist/flows/create.d.ts +1 -2
- package/dist/flows/create.js +6 -6
- package/dist/index.d.ts +86 -20
- package/dist/index.js +216 -20
- package/dist/shared/abis/TaxToken.json +105 -0
- package/dist/shared/abis/TokenManager2.json +60 -0
- package/dist/shared/abis/common.d.ts +83 -2
- package/dist/shared/abis/common.js +253 -2
- package/dist/shared/abis/index.d.ts +6 -5
- package/dist/shared/abis/index.js +7 -5
- package/dist/shared/clients/blockrazor.js +25 -39
- package/dist/shared/clients/club48.d.ts +2 -2
- package/dist/shared/clients/club48.js +29 -34
- package/dist/shared/clients/emitservice.js +0 -2
- package/dist/shared/clients/four.d.ts +6 -21
- package/dist/shared/clients/four.js +24 -29
- package/dist/shared/clients/merkle.js +34 -27
- package/dist/shared/constants/addresses.d.ts +1 -1
- package/dist/shared/constants/addresses.js +2 -11
- package/dist/shared/constants/chains.d.ts +1 -1
- package/dist/shared/constants/chains.js +1 -1
- package/dist/shared/constants/gas.d.ts +1 -1
- package/dist/shared/constants/gas.js +6 -2
- package/dist/shared/constants/index.d.ts +0 -3
- package/dist/shared/constants/index.js +0 -1
- package/dist/shared/flap/abi.js +1 -1
- package/dist/shared/flap/constants.d.ts +2 -1
- package/dist/shared/flap/constants.js +4 -3
- package/dist/shared/flap/curve.js +0 -3
- package/dist/shared/flap/errors.d.ts +4 -1
- package/dist/shared/flap/errors.js +1 -20
- package/dist/shared/flap/index.d.ts +5 -5
- package/dist/shared/flap/index.js +5 -5
- package/dist/shared/flap/launch-v6.d.ts +117 -0
- package/dist/shared/flap/launch-v6.js +111 -0
- package/dist/shared/flap/meta.d.ts +18 -22
- package/dist/shared/flap/meta.js +17 -12
- package/dist/shared/flap/permit.js +2 -5
- package/dist/shared/flap/pinata.d.ts +6 -22
- package/dist/shared/flap/pinata.js +26 -21
- package/dist/shared/flap/portal-bundle-merkle/config.d.ts +72 -3
- package/dist/shared/flap/portal-bundle-merkle/config.js +124 -4
- package/dist/shared/flap/portal-bundle-merkle/core.d.ts +4 -0
- package/dist/shared/flap/portal-bundle-merkle/core.js +267 -164
- package/dist/shared/flap/portal-bundle-merkle/create-to-dex.d.ts +4 -17
- package/dist/shared/flap/portal-bundle-merkle/create-to-dex.js +195 -107
- package/dist/shared/flap/portal-bundle-merkle/curve-to-dex.js +92 -100
- package/dist/shared/flap/portal-bundle-merkle/index.d.ts +7 -11
- package/dist/shared/flap/portal-bundle-merkle/index.js +7 -4
- package/dist/shared/flap/portal-bundle-merkle/pancake-proxy.js +68 -71
- package/dist/shared/flap/portal-bundle-merkle/private.js +114 -61
- package/dist/shared/flap/portal-bundle-merkle/swap-buy-first.d.ts +64 -1
- package/dist/shared/flap/portal-bundle-merkle/swap-buy-first.js +247 -66
- package/dist/shared/flap/portal-bundle-merkle/swap.d.ts +71 -2
- package/dist/shared/flap/portal-bundle-merkle/swap.js +410 -103
- package/dist/shared/flap/portal-bundle-merkle/types.d.ts +12 -88
- package/dist/shared/flap/portal-bundle-merkle/utils.d.ts +80 -1
- package/dist/shared/flap/portal-bundle-merkle/utils.js +265 -145
- package/dist/shared/flap/portal-bundle.js +56 -55
- package/dist/shared/flap/portal.d.ts +3 -14
- package/dist/shared/flap/portal.js +26 -49
- package/dist/shared/flap/vanity.d.ts +5 -4
- package/dist/shared/flap/vanity.js +20 -13
- package/dist/shared/flap/vault.d.ts +93 -25
- package/dist/shared/flap/vault.js +126 -75
- package/dist/shared/four/tax-token.d.ts +1 -1
- package/dist/shared/four/tax-token.js +7 -27
- package/dist/shared/index.d.ts +0 -6
- package/dist/shared/index.js +0 -4
- package/dist/utils/airdrop-sweep.d.ts +76 -4
- package/dist/utils/airdrop-sweep.js +55 -42
- package/dist/utils/bundle-helpers.d.ts +243 -9
- package/dist/utils/bundle-helpers.js +584 -10
- package/dist/utils/constants.d.ts +61 -5
- package/dist/utils/constants.js +80 -5
- package/dist/utils/contract-factory.d.ts +4 -2
- package/dist/utils/contract-factory.js +18 -25
- package/dist/utils/erc20.d.ts +89 -7
- package/dist/utils/erc20.js +125 -94
- package/dist/utils/errors.d.ts +1 -12
- package/dist/utils/errors.js +1 -60
- package/dist/utils/holders-maker.d.ts +138 -2
- package/dist/utils/holders-maker.js +661 -26
- package/dist/utils/lp-inspect.d.ts +112 -2
- package/dist/utils/lp-inspect.js +223 -73
- package/dist/utils/mpcExclusive.d.ts +5 -2
- package/dist/utils/mpcExclusive.js +3 -4
- package/dist/utils/private-sale.d.ts +58 -2
- package/dist/utils/private-sale.js +15 -4
- package/dist/utils/provider-factory.d.ts +0 -4
- package/dist/utils/provider-factory.js +0 -10
- package/dist/utils/quote-helpers.d.ts +45 -4
- package/dist/utils/quote-helpers.js +74 -17
- package/dist/utils/stealth-transfer.d.ts +28 -2
- package/dist/utils/stealth-transfer.js +15 -31
- package/dist/utils/swap-helpers.d.ts +15 -2
- package/dist/utils/swap-helpers.js +11 -6
- package/dist/utils/wallet.d.ts +25 -2
- package/dist/utils/wallet.js +10 -13
- package/package.json +4 -160
- package/dist/__tests__/subpath-exports.test.d.ts +0 -1
- package/dist/__tests__/subpath-exports.test.js +0 -64
- package/dist/abis/common.d.ts +0 -85
- package/dist/abis/common.js +0 -264
- package/dist/abis/contracts/TaxToken.json +0 -969
- package/dist/abis/contracts/TokenManager2.json +0 -136
- package/dist/abis/contracts/index.d.ts +0 -5
- package/dist/abis/contracts/index.js +0 -5
- package/dist/abis/flap/index.d.ts +0 -3
- package/dist/abis/flap/index.js +0 -3
- package/dist/abis/flap/portal-events.d.ts +0 -6
- package/dist/abis/flap/portal-events.js +0 -17
- package/dist/abis/flap/portal.d.ts +0 -6
- package/dist/abis/flap/portal.js +0 -37
- package/dist/abis/flap/vault.d.ts +0 -171
- package/dist/abis/flap/vault.js +0 -91
- package/dist/abis/index.d.ts +0 -8
- package/dist/abis/index.js +0 -11
- package/dist/bundle-core/__tests__/config-helpers.test.d.ts +0 -1
- package/dist/bundle-core/__tests__/config-helpers.test.js +0 -28
- package/dist/bundle-core/__tests__/facade-parity.test.d.ts +0 -1
- package/dist/bundle-core/__tests__/facade-parity.test.js +0 -33
- package/dist/bundle-core/__tests__/sign-context-helpers.test.d.ts +0 -1
- package/dist/bundle-core/__tests__/sign-context-helpers.test.js +0 -60
- package/dist/bundle-core/__tests__/sign-fixture.test.d.ts +0 -1
- package/dist/bundle-core/__tests__/sign-fixture.test.js +0 -220
- package/dist/bundle-core/__tests__/sign-fixtures.d.ts +0 -10
- package/dist/bundle-core/__tests__/sign-fixtures.js +0 -16
- package/dist/bundle-core/config-helpers.d.ts +0 -36
- package/dist/bundle-core/config-helpers.js +0 -57
- package/dist/bundle-core/errors.d.ts +0 -50
- package/dist/bundle-core/errors.js +0 -35
- package/dist/bundle-core/four-meme/approve-tokenmanager.d.ts +0 -7
- package/dist/bundle-core/four-meme/approve-tokenmanager.js +0 -99
- package/dist/bundle-core/four-meme/core-helpers.d.ts +0 -8
- package/dist/bundle-core/four-meme/core-helpers.js +0 -40
- package/dist/bundle-core/four-meme/core.d.ts +0 -4
- package/dist/bundle-core/four-meme/core.js +0 -515
- package/dist/bundle-core/four-meme/pancake-proxy.d.ts +0 -28
- package/dist/bundle-core/four-meme/pancake-proxy.js +0 -679
- package/dist/bundle-core/four-meme/private.d.ts +0 -27
- package/dist/bundle-core/four-meme/private.js +0 -465
- package/dist/bundle-core/four-meme/sign-context-helpers.d.ts +0 -2
- package/dist/bundle-core/four-meme/sign-context-helpers.js +0 -2
- package/dist/bundle-core/four-meme/swap-buy-first.d.ts +0 -8
- package/dist/bundle-core/four-meme/swap-buy-first.js +0 -493
- package/dist/bundle-core/four-meme/swap-hop-helpers.d.ts +0 -6
- package/dist/bundle-core/four-meme/swap-hop-helpers.js +0 -63
- package/dist/bundle-core/four-meme/swap-internal.d.ts +0 -3
- package/dist/bundle-core/four-meme/swap-internal.js +0 -18
- package/dist/bundle-core/four-meme/swap-sign-helpers.d.ts +0 -27
- package/dist/bundle-core/four-meme/swap-sign-helpers.js +0 -105
- package/dist/bundle-core/four-meme/swap.d.ts +0 -17
- package/dist/bundle-core/four-meme/swap.js +0 -505
- package/dist/bundle-core/four-meme/types/buy-first.d.ts +0 -50
- package/dist/bundle-core/four-meme/types/buy-first.js +0 -1
- package/dist/bundle-core/four-meme/types/core-flow.d.ts +0 -63
- package/dist/bundle-core/four-meme/types/core-flow.js +0 -1
- package/dist/bundle-core/four-meme/types/index.d.ts +0 -600
- package/dist/bundle-core/four-meme/types/index.js +0 -1
- package/dist/bundle-core/four-meme/types/swap-internal.d.ts +0 -19
- package/dist/bundle-core/four-meme/types/swap-internal.js +0 -1
- package/dist/bundle-core/four-meme/types.d.ts +0 -1
- package/dist/bundle-core/four-meme/types.js +0 -1
- package/dist/bundle-core/four-meme/utils-disperse.d.ts +0 -7
- package/dist/bundle-core/four-meme/utils-disperse.js +0 -396
- package/dist/bundle-core/four-meme/utils-pairwise.d.ts +0 -8
- package/dist/bundle-core/four-meme/utils-pairwise.js +0 -328
- package/dist/bundle-core/four-meme/utils-sweep.d.ts +0 -8
- package/dist/bundle-core/four-meme/utils-sweep.js +0 -744
- package/dist/bundle-core/index.d.ts +0 -8
- package/dist/bundle-core/index.js +0 -8
- package/dist/bundle-core/internal.d.ts +0 -21
- package/dist/bundle-core/internal.js +0 -182
- package/dist/bundle-core/sign-context-helpers.d.ts +0 -25
- package/dist/bundle-core/sign-context-helpers.js +0 -67
- package/dist/bundle-core/submit.d.ts +0 -293
- package/dist/bundle-core/submit.js +0 -727
- package/dist/bundle-core/types/index.d.ts +0 -8
- package/dist/bundle-core/types/index.js +0 -1
- package/dist/bundle-core/types.d.ts +0 -1
- package/dist/bundle-core/types.js +0 -1
- package/dist/chains/bsc/four/utils-disperse.d.ts +0 -1
- package/dist/chains/bsc/four/utils-disperse.js +0 -1
- package/dist/chains/bsc/four/utils-pairwise.d.ts +0 -1
- package/dist/chains/bsc/four/utils-pairwise.js +0 -1
- package/dist/chains/bsc/four/utils-sweep.d.ts +0 -1
- package/dist/chains/bsc/four/utils-sweep.js +0 -1
- package/dist/chains/bsc/iro.d.ts +0 -5
- package/dist/chains/bsc/iro.js +0 -4
- package/dist/chains/bsc/pancake/bundle-buy-first-helpers.d.ts +0 -159
- package/dist/chains/bsc/pancake/bundle-buy-first-helpers.js +0 -117
- package/dist/chains/bsc/pancake/bundle-swap-helpers.d.ts +0 -241
- package/dist/chains/bsc/pancake/bundle-swap-helpers.js +0 -565
- package/dist/chains/eni/flat-aliases.d.ts +0 -10
- package/dist/chains/eni/flat-aliases.js +0 -8
- package/dist/chains/eni/submit.d.ts +0 -43
- package/dist/chains/eni/submit.js +0 -286
- package/dist/chains/xlayer/eip7702/flat-aliases.d.ts +0 -13
- package/dist/chains/xlayer/eip7702/flat-aliases.js +0 -10
- package/dist/chains/xlayer/eip7702/multi-hop-transfer-helpers.d.ts +0 -79
- package/dist/chains/xlayer/eip7702/multi-hop-transfer-helpers.js +0 -1
- package/dist/chains/xlayer/eip7702/transfer-context-helpers.d.ts +0 -26
- package/dist/chains/xlayer/eip7702/transfer-context-helpers.js +0 -57
- package/dist/chains/xlayer/eip7702/volume-helpers.d.ts +0 -148
- package/dist/chains/xlayer/eip7702/volume-helpers.js +0 -48
- package/dist/chains/xlayer/eoa/eoa-bundle-swap-helpers.d.ts +0 -126
- package/dist/chains/xlayer/eoa/eoa-bundle-swap-helpers.js +0 -228
- package/dist/contracts/tm-bundle-helpers.d.ts +0 -88
- package/dist/contracts/tm-bundle-helpers.js +0 -72
- package/dist/contracts/tm-bundle-merkle/utils-disperse.d.ts +0 -1
- package/dist/contracts/tm-bundle-merkle/utils-disperse.js +0 -1
- package/dist/contracts/tm-bundle-merkle/utils-pairwise.d.ts +0 -1
- package/dist/contracts/tm-bundle-merkle/utils-pairwise.js +0 -1
- package/dist/contracts/tm-bundle-merkle/utils-sweep.d.ts +0 -1
- package/dist/contracts/tm-bundle-merkle/utils-sweep.js +0 -1
- package/dist/dex/direct-router-helpers.d.ts +0 -264
- package/dist/dex/direct-router-helpers.js +0 -539
- package/dist/dex/types.d.ts +0 -81
- package/dist/dex/types.js +0 -1
- package/dist/exports/root-bundle-and-tooling.d.ts +0 -27
- package/dist/exports/root-bundle-and-tooling.js +0 -30
- package/dist/exports/root-eni-and-bsc-iro.d.ts +0 -26
- package/dist/exports/root-eni-and-bsc-iro.js +0 -66
- package/dist/exports/root-foundations.d.ts +0 -35
- package/dist/exports/root-foundations.js +0 -70
- package/dist/exports/root-swap-dex-and-xlayer.d.ts +0 -30
- package/dist/exports/root-swap-dex-and-xlayer.js +0 -78
- package/dist/flap/index.d.ts +0 -10
- package/dist/flap/index.js +0 -8
- package/dist/flows/index.d.ts +0 -1
- package/dist/flows/index.js +0 -1
- package/dist/merkle/index.d.ts +0 -12
- package/dist/merkle/index.js +0 -11
- package/dist/shared/clients/index.d.ts +0 -8
- package/dist/shared/clients/index.js +0 -8
- package/dist/shared/constants/quote.d.ts +0 -30
- package/dist/shared/constants/quote.js +0 -37
- package/dist/shared/flap/portal-bundle-merkle/core-helpers.d.ts +0 -32
- package/dist/shared/flap/portal-bundle-merkle/core-helpers.js +0 -83
- package/dist/shared/flap/portal-bundle-merkle/swap-buy-first-helpers.d.ts +0 -125
- package/dist/shared/flap/portal-bundle-merkle/swap-buy-first-helpers.js +0 -113
- package/dist/shared/flap/portal-bundle-merkle/swap-helpers.d.ts +0 -149
- package/dist/shared/flap/portal-bundle-merkle/swap-helpers.js +0 -259
- package/dist/shared/flap/portal-create-token.d.ts +0 -79
- package/dist/shared/flap/portal-create-token.js +0 -261
- package/dist/shared/foundation/dex/v3-path.d.ts +0 -6
- package/dist/shared/foundation/dex/v3-path.js +0 -35
- package/dist/shared/foundation/gas/bundle-gas.d.ts +0 -49
- package/dist/shared/foundation/gas/bundle-gas.js +0 -93
- package/dist/shared/foundation/gas/profit-hop.d.ts +0 -20
- package/dist/shared/foundation/gas/profit-hop.js +0 -72
- package/dist/shared/foundation/index.d.ts +0 -13
- package/dist/shared/foundation/index.js +0 -12
- package/dist/shared/foundation/nonce/nonce-manager.d.ts +0 -17
- package/dist/shared/foundation/nonce/nonce-manager.js +0 -183
- package/dist/shared/foundation/normalize-unknown.d.ts +0 -9
- package/dist/shared/foundation/normalize-unknown.js +0 -29
- package/dist/shared/foundation/sdk-logger.d.ts +0 -13
- package/dist/shared/foundation/sdk-logger.js +0 -12
- package/dist/shared/foundation/tx/build-request.d.ts +0 -17
- package/dist/shared/foundation/tx/build-request.js +0 -25
- package/dist/shared/foundation/tx/sign-batch.d.ts +0 -5
- package/dist/shared/foundation/tx/sign-batch.js +0 -26
- package/dist/shared/foundation/tx/wallet-sign-patch.d.ts +0 -1
- package/dist/shared/foundation/tx/wallet-sign-patch.js +0 -18
- package/dist/shared/foundation/types/airdrop-sweep.d.ts +0 -79
- package/dist/shared/foundation/types/airdrop-sweep.js +0 -1
- package/dist/shared/foundation/types/erc20.d.ts +0 -65
- package/dist/shared/foundation/types/erc20.js +0 -1
- package/dist/shared/foundation/types/holders-maker.d.ts +0 -64
- package/dist/shared/foundation/types/holders-maker.js +0 -1
- package/dist/shared/foundation/types/index.d.ts +0 -7
- package/dist/shared/foundation/types/index.js +0 -1
- package/dist/shared/foundation/types/lp-inspect.d.ts +0 -102
- package/dist/shared/foundation/types/lp-inspect.js +0 -1
- package/dist/shared/foundation/types/multicall.d.ts +0 -5
- package/dist/shared/foundation/types/multicall.js +0 -1
- package/dist/shared/foundation/types/private-sale.d.ts +0 -35
- package/dist/shared/foundation/types/private-sale.js +0 -1
- package/dist/shared/foundation/types/quote-helpers.d.ts +0 -17
- package/dist/shared/foundation/types/quote-helpers.js +0 -1
- package/dist/types/errors.d.ts +0 -27
- package/dist/types/errors.js +0 -34
- package/dist/utils/holders-maker/addresses.d.ts +0 -12
- package/dist/utils/holders-maker/addresses.js +0 -15
- package/dist/utils/holders-maker/buy-tx.d.ts +0 -44
- package/dist/utils/holders-maker/buy-tx.js +0 -278
- package/dist/utils/holders-maker/constants.d.ts +0 -6
- package/dist/utils/holders-maker/constants.js +0 -7
- package/dist/utils/holders-maker/disperse.d.ts +0 -18
- package/dist/utils/holders-maker/disperse.js +0 -90
- package/dist/utils/holders-maker/routing.d.ts +0 -4
- package/dist/utils/holders-maker/routing.js +0 -45
- package/dist/utils/holders-maker/transfer-tx.d.ts +0 -4
- package/dist/utils/holders-maker/transfer-tx.js +0 -67
- package/dist/utils/holders-maker-helpers.d.ts +0 -9
- package/dist/utils/holders-maker-helpers.js +0 -9
- package/dist/utils/hop-chains.d.ts +0 -35
- package/dist/utils/hop-chains.js +0 -215
- package/dist/utils/lp-inspect-helpers.d.ts +0 -9
- package/dist/utils/lp-inspect-helpers.js +0 -109
- package/dist/utils/types/airdrop-sweep.d.ts +0 -1
- package/dist/utils/types/airdrop-sweep.js +0 -1
- package/dist/utils/types/contract-factory.d.ts +0 -1
- package/dist/utils/types/contract-factory.js +0 -1
- package/dist/utils/types/erc20.d.ts +0 -1
- package/dist/utils/types/erc20.js +0 -1
- package/dist/utils/types/errors.d.ts +0 -1
- package/dist/utils/types/errors.js +0 -1
- package/dist/utils/types/holders-maker.d.ts +0 -1
- package/dist/utils/types/holders-maker.js +0 -1
- package/dist/utils/types/hop-chains.d.ts +0 -8
- package/dist/utils/types/hop-chains.js +0 -1
- package/dist/utils/types/index.d.ts +0 -13
- package/dist/utils/types/index.js +0 -1
- package/dist/utils/types/lp-inspect.d.ts +0 -1
- package/dist/utils/types/lp-inspect.js +0 -1
- package/dist/utils/types/mpc-exclusive.d.ts +0 -5
- package/dist/utils/types/mpc-exclusive.js +0 -1
- package/dist/utils/types/private-sale.d.ts +0 -1
- package/dist/utils/types/private-sale.js +0 -1
- package/dist/utils/types/quote-helpers.d.ts +0 -1
- package/dist/utils/types/quote-helpers.js +0 -1
- package/dist/utils/types/stealth-transfer.d.ts +0 -44
- package/dist/utils/types/stealth-transfer.js +0 -1
- package/dist/utils/types/wallet.d.ts +0 -25
- package/dist/utils/types/wallet.js +0 -1
- package/dist/vanity/index.d.ts +0 -5
- package/dist/vanity/index.js +0 -5
- package/src/abis/contracts/TaxToken.json +0 -969
- package/src/abis/contracts/TokenManager.json +0 -836
- package/src/abis/contracts/TokenManager2.json +0 -136
- package/src/abis/contracts/TokenManagerHelper3.json +0 -993
- /package/dist/{abis/contracts → shared/abis}/TokenManager.json +0 -0
- /package/dist/{abis/contracts → shared/abis}/TokenManagerHelper3.json +0 -0
|
@@ -1,7 +1,1554 @@
|
|
|
1
|
+
import { ethers, Wallet } from 'ethers';
|
|
2
|
+
import { getOptimizedGasPrice, NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields } from '../../../utils/bundle-helpers.js';
|
|
3
|
+
import { BLOCKRAZOR_BUILDER_EOA, calculateTransferFee } from '../../../utils/constants.js';
|
|
4
|
+
import { GAS_LIMITS, CHAINS } from '../../../shared/constants/index.js';
|
|
5
|
+
import { getTxType, getGasPriceConfig, shouldExtractProfit, getProfitRecipient, getBribeAmount } from './config.js';
|
|
6
|
+
// ==================== Gas 常量(使用统一常量) ====================
|
|
7
|
+
const NATIVE_TRANSFER_GAS_LIMIT = GAS_LIMITS.NATIVE_TRANSFER;
|
|
8
|
+
const ERC20_TRANSFER_GAS_LIMIT = GAS_LIMITS.ERC20_TRANSFER;
|
|
9
|
+
const BRIBE_GAS_LIMIT = GAS_LIMITS.BRIBE;
|
|
10
|
+
const PROFIT_HOP_GAS_LIMIT = GAS_LIMITS.NATIVE_TRANSFER; // 利润多跳使用 NATIVE_TRANSFER
|
|
11
|
+
import { getErc20DecimalsMerkle as _getErc20DecimalsMerkle, generateHopWallets as _generateHopWallets, normalizeAmounts as _normalizeAmounts, batchGetBalances as _batchGetBalances, calculateGasLimit as _calculateGasLimit, isNativeTokenAddress as _isNativeTokenAddress } from './internal.js';
|
|
12
|
+
// ==================== 钱包转账按地址收费 ====================
|
|
1
13
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
14
|
+
* 分发(仅签名版本 - 不依赖 Merkle)
|
|
15
|
+
* ✅ 精简版:只负责签名交易,不提交到 Merkle
|
|
16
|
+
* ✅ 优化版:支持 startNonce 参数避免并行调用时的 nonce 冲突
|
|
4
17
|
*/
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
18
|
+
export async function disperseWithBundleMerkle(params) {
|
|
19
|
+
const { fromPrivateKey, recipients, amount, amounts, tokenAddress, tokenDecimals, hopCount = 0, hopPrivateKeys, items, config, startNonce } = params;
|
|
20
|
+
// 快速返回空结果
|
|
21
|
+
if (!recipients || recipients.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
signedTransactions: [],
|
|
24
|
+
hopWallets: undefined
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// 初始化
|
|
28
|
+
const chainIdNum = config.chainId ?? CHAINS.BSC.chainId;
|
|
29
|
+
const chainName = chainIdNum === CHAINS.MONAD.chainId ? 'Monad' : chainIdNum === CHAINS.BSC.chainId ? 'BSC' : `Chain-${chainIdNum}`;
|
|
30
|
+
const provider = new ethers.JsonRpcProvider(config.rpcUrl, {
|
|
31
|
+
chainId: chainIdNum,
|
|
32
|
+
name: chainName
|
|
33
|
+
});
|
|
34
|
+
const mainWallet = new Wallet(fromPrivateKey, provider);
|
|
35
|
+
const isNative = _isNativeTokenAddress(tokenAddress);
|
|
36
|
+
const txType = getTxType(config);
|
|
37
|
+
// ✅ 优化:预处理数据(不需要 RPC)
|
|
38
|
+
const normalizedAmounts = items && items.length > 0
|
|
39
|
+
? items.map(it => (typeof it.amount === 'bigint' ? it.amount.toString() : String(it.amount)))
|
|
40
|
+
: _normalizeAmounts(recipients, amount, amounts);
|
|
41
|
+
const providedHops = (() => {
|
|
42
|
+
if (hopPrivateKeys && hopPrivateKeys.length > 0) {
|
|
43
|
+
if (hopPrivateKeys.length !== recipients.length) {
|
|
44
|
+
throw new Error(`hopPrivateKeys length (${hopPrivateKeys.length}) must match recipients length (${recipients.length})`);
|
|
45
|
+
}
|
|
46
|
+
return hopPrivateKeys.every(h => h.length === 0) ? null : hopPrivateKeys;
|
|
47
|
+
}
|
|
48
|
+
if (items && items.length > 0) {
|
|
49
|
+
const hops = items.map(it => it.hopPrivateKeys ?? []);
|
|
50
|
+
return hops.every(h => h.length === 0) ? null : hops;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
})();
|
|
54
|
+
const hopCountInput = (() => {
|
|
55
|
+
if (items && items.length > 0) {
|
|
56
|
+
const baseArray = Array.isArray(hopCount) ? hopCount : new Array(recipients.length).fill(hopCount);
|
|
57
|
+
return items.map((it, i) => (typeof it.hopCount === 'number' ? it.hopCount : (baseArray[i] ?? 0)));
|
|
58
|
+
}
|
|
59
|
+
return hopCount;
|
|
60
|
+
})();
|
|
61
|
+
const preparedHops = providedHops ?? _generateHopWallets(recipients.length, hopCountInput);
|
|
62
|
+
const hasHops = preparedHops !== null;
|
|
63
|
+
const maxHopCount = hasHops ? Math.max(...preparedHops.map(h => h.length)) : 0;
|
|
64
|
+
const finalGasLimit = _calculateGasLimit(config, isNative, hasHops, maxHopCount);
|
|
65
|
+
const nativeGasLimit = (config.prefer21000ForNative ?? false) ? BRIBE_GAS_LIMIT : finalGasLimit;
|
|
66
|
+
const signedTxs = [];
|
|
67
|
+
const extractProfit = shouldExtractProfit(config);
|
|
68
|
+
const profitAddr = getProfitRecipient();
|
|
69
|
+
let totalProfit = 0n;
|
|
70
|
+
let totalAmountBeforeProfit = 0n;
|
|
71
|
+
let profitHopWallets; // ✅ 收集利润多跳钱包
|
|
72
|
+
// ✅ 优化:并行获取 gasPrice 和 nonces(或使用传入的 startNonce)
|
|
73
|
+
const nonceManager = new NonceManager(provider);
|
|
74
|
+
if (!preparedHops) {
|
|
75
|
+
// ========== 无多跳:直接批量转账 ==========
|
|
76
|
+
// ✅ BSC 链贿赂支持
|
|
77
|
+
const bribeAmount = chainIdNum === CHAINS.BSC.chainId ? getBribeAmount(config) : 0n;
|
|
78
|
+
const needBribeTx = bribeAmount > 0n;
|
|
79
|
+
const extraTxCount = (extractProfit ? 1 : 0) + (needBribeTx ? 1 : 0);
|
|
80
|
+
const totalTxCount = recipients.length + extraTxCount;
|
|
81
|
+
// ✅ 优化:并行获取 gasPrice 和 nonces
|
|
82
|
+
const [gasPrice, nonces] = await Promise.all([
|
|
83
|
+
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
84
|
+
startNonce !== undefined
|
|
85
|
+
? Promise.resolve(Array.from({ length: totalTxCount }, (_, i) => startNonce + i))
|
|
86
|
+
: nonceManager.getNextNonceBatch(mainWallet, totalTxCount)
|
|
87
|
+
]);
|
|
88
|
+
// ✅ 贿赂交易放在最前面
|
|
89
|
+
let nonceOffset = 0;
|
|
90
|
+
if (needBribeTx) {
|
|
91
|
+
const bribeTx = await mainWallet.signTransaction({
|
|
92
|
+
to: BLOCKRAZOR_BUILDER_EOA,
|
|
93
|
+
value: bribeAmount,
|
|
94
|
+
nonce: nonces[nonceOffset++],
|
|
95
|
+
gasPrice,
|
|
96
|
+
gasLimit: BRIBE_GAS_LIMIT,
|
|
97
|
+
chainId: chainIdNum,
|
|
98
|
+
type: txType
|
|
99
|
+
});
|
|
100
|
+
signedTxs.push(bribeTx);
|
|
101
|
+
console.log(`[disperse] 贿赂交易已添加: ${ethers.formatEther(bribeAmount)} BNB`);
|
|
102
|
+
}
|
|
103
|
+
if (isNative) {
|
|
104
|
+
// ✅ 原生币:按地址数量收取固定费用,全额转账
|
|
105
|
+
if (extractProfit) {
|
|
106
|
+
totalProfit = calculateTransferFee(chainIdNum, recipients.length);
|
|
107
|
+
console.log(`[disperse Native] 按地址收费: ${recipients.length} 个地址 × ${ethers.formatEther(calculateTransferFee(chainIdNum, 1))} = ${ethers.formatEther(totalProfit)} 原生币`);
|
|
108
|
+
}
|
|
109
|
+
const txDataList = recipients.map((to, i) => {
|
|
110
|
+
const originalAmount = ethers.parseEther(normalizedAmounts[i]);
|
|
111
|
+
totalAmountBeforeProfit += originalAmount;
|
|
112
|
+
return { to, value: originalAmount, nonce: nonces[nonceOffset + i] };
|
|
113
|
+
});
|
|
114
|
+
// ✅ 并行签名所有交易
|
|
115
|
+
const txPromises = txDataList.map(({ to, value, nonce }) => mainWallet.signTransaction({
|
|
116
|
+
to,
|
|
117
|
+
value,
|
|
118
|
+
nonce,
|
|
119
|
+
gasPrice,
|
|
120
|
+
gasLimit: nativeGasLimit,
|
|
121
|
+
chainId: chainIdNum,
|
|
122
|
+
type: txType
|
|
123
|
+
}));
|
|
124
|
+
signedTxs.push(...(await Promise.all(txPromises)));
|
|
125
|
+
// ✅ 利润多跳转账(强制 2 跳中转)
|
|
126
|
+
if (extractProfit && totalProfit > 0n) {
|
|
127
|
+
const profitHopResult = await buildProfitHopTransactions({
|
|
128
|
+
provider,
|
|
129
|
+
payerWallet: mainWallet,
|
|
130
|
+
profitAmount: totalProfit,
|
|
131
|
+
profitRecipient: profitAddr,
|
|
132
|
+
hopCount: PROFIT_HOP_COUNT,
|
|
133
|
+
gasPrice,
|
|
134
|
+
chainId: chainIdNum,
|
|
135
|
+
txType,
|
|
136
|
+
startNonce: nonces[nonceOffset + recipients.length]
|
|
137
|
+
});
|
|
138
|
+
signedTxs.push(...profitHopResult.signedTransactions);
|
|
139
|
+
profitHopWallets = profitHopResult.hopWallets; // ✅ 收集利润多跳钱包
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// ✅ ERC20:并行获取 decimals(传入 chainIdNum 避免 NETWORK_ERROR)
|
|
144
|
+
const decimals = tokenDecimals ?? (await _getErc20DecimalsMerkle(provider, tokenAddress, chainIdNum));
|
|
145
|
+
const iface = new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
146
|
+
// ✅ 先计算所有原始金额
|
|
147
|
+
const originalAmounts = [];
|
|
148
|
+
for (let i = 0; i < recipients.length; i++) {
|
|
149
|
+
const originalAmount = ethers.parseUnits(normalizedAmounts[i], decimals);
|
|
150
|
+
originalAmounts.push(originalAmount);
|
|
151
|
+
totalAmountBeforeProfit += originalAmount;
|
|
152
|
+
}
|
|
153
|
+
// ✅ 按地址数量收取固定费用(原生代币)
|
|
154
|
+
let nativeProfitAmount = 0n;
|
|
155
|
+
if (extractProfit) {
|
|
156
|
+
nativeProfitAmount = calculateTransferFee(chainIdNum, recipients.length);
|
|
157
|
+
totalProfit = nativeProfitAmount;
|
|
158
|
+
console.log(`[disperse ERC20] 按地址收费: ${recipients.length} 个地址 × ${ethers.formatEther(calculateTransferFee(chainIdNum, 1))} = ${ethers.formatEther(nativeProfitAmount)} 原生币`);
|
|
159
|
+
}
|
|
160
|
+
// ✅ ERC20 全额分发,费用以原生币单独支付
|
|
161
|
+
const txDataList = recipients.map((to, i) => {
|
|
162
|
+
const originalAmount = originalAmounts[i];
|
|
163
|
+
const data = iface.encodeFunctionData('transfer', [to, originalAmount]);
|
|
164
|
+
return { data, nonce: nonces[nonceOffset + i] };
|
|
165
|
+
});
|
|
166
|
+
// ✅ 并行签名所有交易
|
|
167
|
+
const txPromises = txDataList.map(({ data, nonce }) => mainWallet.signTransaction({
|
|
168
|
+
to: tokenAddress,
|
|
169
|
+
data,
|
|
170
|
+
value: 0n,
|
|
171
|
+
nonce,
|
|
172
|
+
gasPrice,
|
|
173
|
+
gasLimit: finalGasLimit,
|
|
174
|
+
chainId: chainIdNum,
|
|
175
|
+
type: txType
|
|
176
|
+
}));
|
|
177
|
+
signedTxs.push(...(await Promise.all(txPromises)));
|
|
178
|
+
// ✅ 费用多跳转账(强制 2 跳中转)
|
|
179
|
+
if (extractProfit && nativeProfitAmount > 0n) {
|
|
180
|
+
const profitHopResult = await buildProfitHopTransactions({
|
|
181
|
+
provider,
|
|
182
|
+
payerWallet: mainWallet,
|
|
183
|
+
profitAmount: nativeProfitAmount,
|
|
184
|
+
profitRecipient: profitAddr,
|
|
185
|
+
hopCount: PROFIT_HOP_COUNT,
|
|
186
|
+
gasPrice,
|
|
187
|
+
chainId: chainIdNum,
|
|
188
|
+
txType,
|
|
189
|
+
startNonce: nonces[nonceOffset + recipients.length]
|
|
190
|
+
});
|
|
191
|
+
signedTxs.push(...profitHopResult.signedTransactions);
|
|
192
|
+
profitHopWallets = profitHopResult.hopWallets; // ✅ 收集利润多跳钱包
|
|
193
|
+
console.log(`[disperse ERC20] 利润多跳已添加: ${ethers.formatEther(nativeProfitAmount)} 原生币`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// ========== 有多跳:构建跳转链 ==========
|
|
199
|
+
// ✅ BSC 链贿赂支持
|
|
200
|
+
const bribeAmountHop = chainIdNum === CHAINS.BSC.chainId ? getBribeAmount(config) : 0n;
|
|
201
|
+
const needBribeTxHop = bribeAmountHop > 0n;
|
|
202
|
+
// ✅ 优化:并行获取 gasPrice 和 decimals
|
|
203
|
+
const [gasPrice, decimals] = await Promise.all([
|
|
204
|
+
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
205
|
+
isNative ? Promise.resolve(18) : Promise.resolve(tokenDecimals ?? await _getErc20DecimalsMerkle(provider, tokenAddress, chainIdNum))
|
|
206
|
+
]);
|
|
207
|
+
const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
208
|
+
// ✅ Gas limit 设置(与 swap.ts 保持一致)
|
|
209
|
+
// - 原生代币转账:使用 GAS_LIMITS.NATIVE_TRANSFER(固定)
|
|
210
|
+
// - ERC20 转账:使用 GAS_LIMITS.ERC20_TRANSFER(固定,因为 gas 波动较大)
|
|
211
|
+
const nativeTransferGasLimit = NATIVE_TRANSFER_GAS_LIMIT;
|
|
212
|
+
const erc20TransferGasLimit = GAS_LIMITS.ERC20_TRANSFER;
|
|
213
|
+
// ✅ 原生代币多跳的 gas 费(每跳需要 21055)
|
|
214
|
+
const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
|
|
215
|
+
// ✅ ERC20 多跳时,中转钱包需要执行 2 笔交易(转 gas + 转 ERC20)
|
|
216
|
+
const erc20HopGasFee = erc20TransferGasLimit * gasPrice;
|
|
217
|
+
const nativeHopGasFeeForErc20 = nativeTransferGasLimit * gasPrice;
|
|
218
|
+
// ✅ 优化:预先计算主钱包需要的总 nonce 数量
|
|
219
|
+
// - 原生代币多跳:主钱包只需要 1 个 nonce(一笔转账包含后续所有 gas)
|
|
220
|
+
// - ERC20 多跳:主钱包需要 2 个 nonce(转 gas + 转 ERC20)
|
|
221
|
+
let mainWalletNonceCount = 0;
|
|
222
|
+
// 贿赂交易需要 1 个额外的 nonce
|
|
223
|
+
if (needBribeTxHop)
|
|
224
|
+
mainWalletNonceCount += 1;
|
|
225
|
+
for (let i = 0; i < recipients.length; i++) {
|
|
226
|
+
const hopChain = preparedHops[i];
|
|
227
|
+
if (hopChain.length === 0) {
|
|
228
|
+
// 无跳转:1 个 nonce
|
|
229
|
+
mainWalletNonceCount += 1;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// 有跳转:
|
|
233
|
+
// - Native: 1(一笔转账)
|
|
234
|
+
// - ERC20: 2(转 gas + 转 ERC20)
|
|
235
|
+
mainWalletNonceCount += isNative ? 1 : 2;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// 利润交易需要额外 1 个 nonce
|
|
239
|
+
if (extractProfit)
|
|
240
|
+
mainWalletNonceCount += 1;
|
|
241
|
+
// ✅ 一次性获取所有主钱包的 nonces
|
|
242
|
+
const allMainNonces = startNonce !== undefined
|
|
243
|
+
? Array.from({ length: mainWalletNonceCount }, (_, i) => startNonce + i)
|
|
244
|
+
: await nonceManager.getNextNonceBatch(mainWallet, mainWalletNonceCount);
|
|
245
|
+
let mainNonceIdx = 0;
|
|
246
|
+
// ✅ 贿赂交易放在最前面(多跳场景)
|
|
247
|
+
if (needBribeTxHop) {
|
|
248
|
+
const bribeTx = await mainWallet.signTransaction({
|
|
249
|
+
to: BLOCKRAZOR_BUILDER_EOA,
|
|
250
|
+
value: bribeAmountHop,
|
|
251
|
+
nonce: allMainNonces[mainNonceIdx++],
|
|
252
|
+
gasPrice,
|
|
253
|
+
gasLimit: BRIBE_GAS_LIMIT,
|
|
254
|
+
chainId: chainIdNum,
|
|
255
|
+
type: txType
|
|
256
|
+
});
|
|
257
|
+
signedTxs.push(bribeTx);
|
|
258
|
+
console.log(`[disperse with hops] 贿赂交易已添加: ${ethers.formatEther(bribeAmountHop)} BNB`);
|
|
259
|
+
}
|
|
260
|
+
const txsToSign = [];
|
|
261
|
+
// ✅ 按地址数量收取固定费用(原生代币),不再扣除转账金额
|
|
262
|
+
if (extractProfit) {
|
|
263
|
+
totalProfit = calculateTransferFee(chainIdNum, recipients.length);
|
|
264
|
+
console.log(`[disperse with hops] 按地址收费: ${recipients.length} 个地址 × ${ethers.formatEther(calculateTransferFee(chainIdNum, 1))} = ${ethers.formatEther(totalProfit)} 原生币`);
|
|
265
|
+
}
|
|
266
|
+
for (let i = 0; i < recipients.length; i++) {
|
|
267
|
+
const finalRecipient = recipients[i];
|
|
268
|
+
const originalAmountWei = isNative
|
|
269
|
+
? ethers.parseEther(normalizedAmounts[i])
|
|
270
|
+
: ethers.parseUnits(normalizedAmounts[i], decimals);
|
|
271
|
+
totalAmountBeforeProfit += originalAmountWei;
|
|
272
|
+
const amountWei = originalAmountWei; // 全额转账
|
|
273
|
+
const hopChain = preparedHops[i];
|
|
274
|
+
if (hopChain.length === 0) {
|
|
275
|
+
// 无跳转:直接转账
|
|
276
|
+
const nonce = allMainNonces[mainNonceIdx++];
|
|
277
|
+
if (isNative) {
|
|
278
|
+
txsToSign.push({
|
|
279
|
+
wallet: mainWallet,
|
|
280
|
+
tx: {
|
|
281
|
+
to: finalRecipient,
|
|
282
|
+
value: amountWei,
|
|
283
|
+
nonce,
|
|
284
|
+
gasPrice,
|
|
285
|
+
gasLimit: nativeGasLimit,
|
|
286
|
+
chainId: chainIdNum,
|
|
287
|
+
type: txType
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const data = iface.encodeFunctionData('transfer', [finalRecipient, amountWei]);
|
|
293
|
+
txsToSign.push({
|
|
294
|
+
wallet: mainWallet,
|
|
295
|
+
tx: {
|
|
296
|
+
to: tokenAddress,
|
|
297
|
+
data,
|
|
298
|
+
value: 0n,
|
|
299
|
+
nonce,
|
|
300
|
+
gasPrice,
|
|
301
|
+
gasLimit: finalGasLimit,
|
|
302
|
+
chainId: chainIdNum,
|
|
303
|
+
type: txType
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
// 有跳转
|
|
310
|
+
// ✅ 支持 string 和 GeneratedWallet 两种类型
|
|
311
|
+
const fullChain = [mainWallet, ...hopChain.map(w => new Wallet(typeof w === 'string' ? w : w.privateKey, provider))];
|
|
312
|
+
const addresses = [...fullChain.map(w => w.address), finalRecipient];
|
|
313
|
+
if (isNative) {
|
|
314
|
+
// ========== 原生代币多跳:gas 包含在转账金额中逐层传递 ==========
|
|
315
|
+
for (let j = 0; j < addresses.length - 1; j++) {
|
|
316
|
+
const fromWallet = fullChain[j];
|
|
317
|
+
const toAddress = addresses[j + 1];
|
|
318
|
+
const nonce = j === 0 ? allMainNonces[mainNonceIdx++] : 0;
|
|
319
|
+
const remainingHops = addresses.length - 2 - j;
|
|
320
|
+
const additionalGas = nativeHopGasFee * BigInt(remainingHops);
|
|
321
|
+
const transferValue = amountWei + additionalGas;
|
|
322
|
+
txsToSign.push({
|
|
323
|
+
wallet: fromWallet,
|
|
324
|
+
tx: {
|
|
325
|
+
to: toAddress,
|
|
326
|
+
value: transferValue,
|
|
327
|
+
nonce,
|
|
328
|
+
gasPrice,
|
|
329
|
+
gasLimit: nativeTransferGasLimit,
|
|
330
|
+
chainId: chainIdNum,
|
|
331
|
+
type: txType
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// ========== ERC20 多跳:gas 也逐层传递(保护隐私)==========
|
|
338
|
+
// 计算每个中转钱包需要的 gas(从后往前)
|
|
339
|
+
// - 最后一个中转钱包:只需要 ERC20 转账的 gas
|
|
340
|
+
// - 其他中转钱包:需要 转 gas 的 gas + ERC20 转账的 gas + 后续所有的 gas
|
|
341
|
+
const hopGasNeeds = [];
|
|
342
|
+
for (let j = hopChain.length - 1; j >= 0; j--) {
|
|
343
|
+
if (j === hopChain.length - 1) {
|
|
344
|
+
// 最后一个中转钱包:只需要 ERC20 转账的 gas
|
|
345
|
+
hopGasNeeds.unshift(erc20HopGasFee);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// 其他中转钱包:转 gas(21000) + 转 ERC20 + 后续的 gas
|
|
349
|
+
const nextHopGas = hopGasNeeds[0];
|
|
350
|
+
hopGasNeeds.unshift(nativeHopGasFeeForErc20 + erc20HopGasFee + nextHopGas);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// 第一步:主钱包给第一个中转钱包转 gas(包含所有后续的 gas)
|
|
354
|
+
const totalGasForFirstHop = hopGasNeeds[0];
|
|
355
|
+
txsToSign.push({
|
|
356
|
+
wallet: mainWallet,
|
|
357
|
+
tx: {
|
|
358
|
+
to: fullChain[1].address,
|
|
359
|
+
value: totalGasForFirstHop,
|
|
360
|
+
nonce: allMainNonces[mainNonceIdx++],
|
|
361
|
+
gasPrice,
|
|
362
|
+
gasLimit: nativeTransferGasLimit,
|
|
363
|
+
chainId: chainIdNum,
|
|
364
|
+
type: txType
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
// 第二步:主钱包给第一个中转钱包转 ERC20
|
|
368
|
+
const mainToFirstHopData = iface.encodeFunctionData('transfer', [fullChain[1].address, amountWei]);
|
|
369
|
+
txsToSign.push({
|
|
370
|
+
wallet: mainWallet,
|
|
371
|
+
tx: {
|
|
372
|
+
to: tokenAddress,
|
|
373
|
+
data: mainToFirstHopData,
|
|
374
|
+
value: 0n,
|
|
375
|
+
nonce: allMainNonces[mainNonceIdx++],
|
|
376
|
+
gasPrice,
|
|
377
|
+
gasLimit: erc20TransferGasLimit,
|
|
378
|
+
chainId: chainIdNum,
|
|
379
|
+
type: txType
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
// 第三步:中转钱包逐层传递(gas 和 ERC20)
|
|
383
|
+
for (let j = 1; j < fullChain.length; j++) {
|
|
384
|
+
const fromWallet = fullChain[j];
|
|
385
|
+
const toAddress = addresses[j + 1]; // 下一个地址(可能是中转钱包或最终接收者)
|
|
386
|
+
const isLastHop = j === fullChain.length - 1;
|
|
387
|
+
if (!isLastHop) {
|
|
388
|
+
// 非最后一个中转钱包:先转 gas 给下一个中转钱包
|
|
389
|
+
const gasToTransfer = hopGasNeeds[j]; // 下一个中转钱包需要的 gas
|
|
390
|
+
txsToSign.push({
|
|
391
|
+
wallet: fromWallet,
|
|
392
|
+
tx: {
|
|
393
|
+
to: toAddress,
|
|
394
|
+
value: gasToTransfer,
|
|
395
|
+
nonce: 0, // 中转钱包的第一笔交易
|
|
396
|
+
gasPrice,
|
|
397
|
+
gasLimit: nativeTransferGasLimit,
|
|
398
|
+
chainId: chainIdNum,
|
|
399
|
+
type: txType
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
// 再转 ERC20
|
|
403
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, amountWei]);
|
|
404
|
+
txsToSign.push({
|
|
405
|
+
wallet: fromWallet,
|
|
406
|
+
tx: {
|
|
407
|
+
to: tokenAddress,
|
|
408
|
+
data: erc20Data,
|
|
409
|
+
value: 0n,
|
|
410
|
+
nonce: 1, // 中转钱包的第二笔交易
|
|
411
|
+
gasPrice,
|
|
412
|
+
gasLimit: erc20TransferGasLimit,
|
|
413
|
+
chainId: chainIdNum,
|
|
414
|
+
type: txType
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
// 最后一个中转钱包:只转 ERC20 给最终接收者
|
|
420
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, amountWei]);
|
|
421
|
+
txsToSign.push({
|
|
422
|
+
wallet: fromWallet,
|
|
423
|
+
tx: {
|
|
424
|
+
to: tokenAddress,
|
|
425
|
+
data: erc20Data,
|
|
426
|
+
value: 0n,
|
|
427
|
+
nonce: 0, // 最后一个中转钱包只有一笔交易
|
|
428
|
+
gasPrice,
|
|
429
|
+
gasLimit: erc20TransferGasLimit,
|
|
430
|
+
chainId: chainIdNum,
|
|
431
|
+
type: txType
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// ✅ 并行签名所有交易
|
|
439
|
+
const signedTxsResult = await Promise.all(txsToSign.map(({ wallet, tx }) => wallet.signTransaction(tx)));
|
|
440
|
+
signedTxs.push(...signedTxsResult);
|
|
441
|
+
// ✅ 利润多跳转账(强制 2 跳中转)
|
|
442
|
+
if (extractProfit && totalProfit > 0n) {
|
|
443
|
+
const profitNonce = allMainNonces[mainNonceIdx++];
|
|
444
|
+
const profitHopResult = await buildProfitHopTransactions({
|
|
445
|
+
provider,
|
|
446
|
+
payerWallet: mainWallet,
|
|
447
|
+
profitAmount: totalProfit,
|
|
448
|
+
profitRecipient: profitAddr,
|
|
449
|
+
hopCount: PROFIT_HOP_COUNT,
|
|
450
|
+
gasPrice,
|
|
451
|
+
chainId: chainIdNum,
|
|
452
|
+
txType,
|
|
453
|
+
startNonce: profitNonce
|
|
454
|
+
});
|
|
455
|
+
signedTxs.push(...profitHopResult.signedTransactions);
|
|
456
|
+
profitHopWallets = profitHopResult.hopWallets; // ✅ 收集利润多跳钱包
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// ✅ 简化返回:只返回签名交易、多跳钱包和元数据
|
|
460
|
+
return {
|
|
461
|
+
signedTransactions: signedTxs,
|
|
462
|
+
hopWallets: preparedHops || undefined,
|
|
463
|
+
profitHopWallets, // ✅ 返回利润多跳钱包
|
|
464
|
+
metadata: extractProfit ? {
|
|
465
|
+
totalAmount: ethers.formatEther(totalAmountBeforeProfit),
|
|
466
|
+
profitAmount: ethers.formatEther(totalProfit),
|
|
467
|
+
profitRecipient: profitAddr,
|
|
468
|
+
recipientCount: recipients.length,
|
|
469
|
+
isNative,
|
|
470
|
+
tokenAddress: isNative ? undefined : tokenAddress
|
|
471
|
+
} : undefined
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* 归集(仅签名版本 - 不依赖 Merkle)
|
|
476
|
+
* ✅ 精简版:只负责签名交易,不提交到 Merkle
|
|
477
|
+
* ✅ 优化版:支持 startNonce 参数避免并行调用时的 nonce 冲突
|
|
478
|
+
*/
|
|
479
|
+
export async function sweepWithBundleMerkle(params) {
|
|
480
|
+
const { sourcePrivateKeys, target, targetPrivateKey, ratioPct, ratios, amount, amounts, tokenAddress, tokenDecimals, skipIfInsufficient = true, hopCount = 0, hopPrivateKeys, sources, config, startNonce } = params;
|
|
481
|
+
// 快速返回空结果
|
|
482
|
+
if (!sourcePrivateKeys || sourcePrivateKeys.length === 0) {
|
|
483
|
+
return {
|
|
484
|
+
signedTransactions: [],
|
|
485
|
+
hopWallets: undefined
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// 初始化
|
|
489
|
+
const chainIdNum = config.chainId ?? CHAINS.BSC.chainId;
|
|
490
|
+
const chainName = chainIdNum === CHAINS.MONAD.chainId ? 'Monad' : chainIdNum === CHAINS.BSC.chainId ? 'BSC' : `Chain-${chainIdNum}`;
|
|
491
|
+
const provider = new ethers.JsonRpcProvider(config.rpcUrl, {
|
|
492
|
+
chainId: chainIdNum,
|
|
493
|
+
name: chainName
|
|
494
|
+
});
|
|
495
|
+
const isNative = _isNativeTokenAddress(tokenAddress);
|
|
496
|
+
const txType = getTxType(config);
|
|
497
|
+
// ✅ 如果提供了 targetPrivateKey,创建 targetWallet 用于支付利润
|
|
498
|
+
const targetWallet = targetPrivateKey ? new Wallet(targetPrivateKey, provider) : null;
|
|
499
|
+
// 归集比例处理
|
|
500
|
+
const clamp = (n) => (typeof n === 'number' && Number.isFinite(n)
|
|
501
|
+
? Math.max(0, Math.min(100, Math.floor(n)))
|
|
502
|
+
: undefined);
|
|
503
|
+
const ratio = clamp(ratioPct);
|
|
504
|
+
// 生成跳转钱包
|
|
505
|
+
// 统一 sources 输入
|
|
506
|
+
const actualKeys = (() => {
|
|
507
|
+
if (sources && sources.length > 0)
|
|
508
|
+
return sources.map(s => s.privateKey);
|
|
509
|
+
return sourcePrivateKeys;
|
|
510
|
+
})();
|
|
511
|
+
// hop 私钥链优先使用传入
|
|
512
|
+
const providedHops = (() => {
|
|
513
|
+
if (hopPrivateKeys && hopPrivateKeys.length > 0) {
|
|
514
|
+
if (hopPrivateKeys.length !== actualKeys.length) {
|
|
515
|
+
throw new Error(`hopPrivateKeys length (${hopPrivateKeys.length}) must match sourcePrivateKeys length (${actualKeys.length})`);
|
|
516
|
+
}
|
|
517
|
+
return hopPrivateKeys.every(h => h.length === 0) ? null : hopPrivateKeys;
|
|
518
|
+
}
|
|
519
|
+
if (sources && sources.length > 0) {
|
|
520
|
+
const hops = sources.map(s => s.hopPrivateKeys ?? []);
|
|
521
|
+
return hops.every(h => h.length === 0) ? null : hops;
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
})();
|
|
525
|
+
// hop 数量数组
|
|
526
|
+
const hopCountInput = (() => {
|
|
527
|
+
if (sources && sources.length > 0) {
|
|
528
|
+
const baseArray = Array.isArray(hopCount) ? hopCount : new Array(actualKeys.length).fill(hopCount);
|
|
529
|
+
return sources.map((s, i) => (typeof s.hopCount === 'number' ? s.hopCount : (baseArray[i] ?? 0)));
|
|
530
|
+
}
|
|
531
|
+
return hopCount;
|
|
532
|
+
})();
|
|
533
|
+
const preparedHops = providedHops ?? _generateHopWallets(actualKeys.length, hopCountInput);
|
|
534
|
+
const hasHops = preparedHops !== null;
|
|
535
|
+
const maxHopCount = hasHops ? Math.max(...preparedHops.map(h => h.length)) : 0;
|
|
536
|
+
// 智能计算 Gas Limit
|
|
537
|
+
const finalGasLimit = _calculateGasLimit(config, isNative, hasHops, maxHopCount);
|
|
538
|
+
const nativeGasLimit = (config.prefer21000ForNative ?? false) ? BRIBE_GAS_LIMIT : finalGasLimit;
|
|
539
|
+
const signedTxs = [];
|
|
540
|
+
// ✅ 利润提取配置
|
|
541
|
+
const extractProfit = shouldExtractProfit(config);
|
|
542
|
+
const profitAddr = getProfitRecipient();
|
|
543
|
+
let totalProfit = 0n;
|
|
544
|
+
let totalAmountBeforeProfit = 0n;
|
|
545
|
+
let profitHopWallets; // ✅ 收集利润多跳钱包
|
|
546
|
+
// ✅ BSC 链贿赂支持(由 targetWallet 支付,如果提供了 targetPrivateKey)
|
|
547
|
+
const bribeAmount = chainIdNum === CHAINS.BSC.chainId ? getBribeAmount(config) : 0n;
|
|
548
|
+
const needBribeTx = bribeAmount > 0n && targetWallet !== null;
|
|
549
|
+
const bribeSignedTx = [];
|
|
550
|
+
// ✅ 修复 nonce 冲突:统一管理 targetWallet 的 nonce
|
|
551
|
+
// 贿赂交易和利润多跳交易都由 targetWallet 签名,需要一次性获取所有 nonce
|
|
552
|
+
let targetWalletNonceStart;
|
|
553
|
+
let targetWalletNonceUsed = 0;
|
|
554
|
+
if (targetWallet && (needBribeTx || extractProfit)) {
|
|
555
|
+
// 预先获取 targetWallet 的 pending nonce,后续按顺序使用
|
|
556
|
+
targetWalletNonceStart = await provider.getTransactionCount(targetWallet.address, 'pending');
|
|
557
|
+
console.log(`[sweep] targetWallet nonce 起点: ${targetWalletNonceStart}`);
|
|
558
|
+
}
|
|
559
|
+
// ✅ 贿赂交易放在最前面(由 targetWallet 签名)
|
|
560
|
+
if (needBribeTx && targetWallet && targetWalletNonceStart !== undefined) {
|
|
561
|
+
const bribeGasPrice = await getOptimizedGasPrice(provider, getGasPriceConfig(config));
|
|
562
|
+
const bribeNonce = targetWalletNonceStart + targetWalletNonceUsed;
|
|
563
|
+
targetWalletNonceUsed += 1; // ✅ 递增 nonce 计数
|
|
564
|
+
const bribeTx = await targetWallet.signTransaction({
|
|
565
|
+
to: BLOCKRAZOR_BUILDER_EOA,
|
|
566
|
+
value: bribeAmount,
|
|
567
|
+
nonce: bribeNonce,
|
|
568
|
+
gasPrice: bribeGasPrice,
|
|
569
|
+
gasLimit: BRIBE_GAS_LIMIT,
|
|
570
|
+
chainId: chainIdNum,
|
|
571
|
+
type: txType
|
|
572
|
+
});
|
|
573
|
+
bribeSignedTx.push(bribeTx);
|
|
574
|
+
console.log(`[sweep] 贿赂交易已添加: ${ethers.formatEther(bribeAmount)} BNB (by targetWallet, nonce=${bribeNonce})`);
|
|
575
|
+
}
|
|
576
|
+
if (!preparedHops) {
|
|
577
|
+
// ========== 无多跳:直接批量归集 ==========
|
|
578
|
+
const wallets = actualKeys.map(pk => new Wallet(pk, provider));
|
|
579
|
+
const addresses = wallets.map(w => w.address);
|
|
580
|
+
const nonceManager = new NonceManager(provider);
|
|
581
|
+
// ✅ 优化:并行获取 gasPrice 和余额
|
|
582
|
+
if (isNative) {
|
|
583
|
+
// ✅ 原生币:并行获取 gasPrice 和余额
|
|
584
|
+
const [gasPrice, balances] = await Promise.all([
|
|
585
|
+
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
586
|
+
_batchGetBalances(provider, addresses)
|
|
587
|
+
]);
|
|
588
|
+
const gasCostBase = nativeGasLimit * gasPrice;
|
|
589
|
+
// ✅ 利润多跳需要 PROFIT_HOP_COUNT + 1 笔交易的 gas(payer → hop1 → hop2 → 利润地址)
|
|
590
|
+
const profitTxGas = PROFIT_HOP_GAS_LIMIT * gasPrice * BigInt(PROFIT_HOP_COUNT + 1);
|
|
591
|
+
// ✅ 第一步:计算所有钱包的归集金额和利润,找出归集金额最大的钱包
|
|
592
|
+
const sweepAmounts = [];
|
|
593
|
+
let maxSweepIndex = -1;
|
|
594
|
+
let maxSweepAmount = 0n;
|
|
595
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
596
|
+
const bal = balances[i];
|
|
597
|
+
let toSend = 0n;
|
|
598
|
+
const ratioForI = (() => {
|
|
599
|
+
if (sources && sources[i] && sources[i].ratioPct !== undefined)
|
|
600
|
+
return clamp(sources[i].ratioPct);
|
|
601
|
+
if (ratios && ratios[i] !== undefined)
|
|
602
|
+
return clamp(ratios[i]);
|
|
603
|
+
return ratio;
|
|
604
|
+
})();
|
|
605
|
+
const amountStrForI = (() => {
|
|
606
|
+
if (sources && sources[i] && sources[i].amount !== undefined)
|
|
607
|
+
return String(sources[i].amount);
|
|
608
|
+
if (amounts && amounts[i] !== undefined)
|
|
609
|
+
return String(amounts[i]);
|
|
610
|
+
return amount !== undefined ? String(amount) : undefined;
|
|
611
|
+
})();
|
|
612
|
+
// ✅ 只有支付者需要 2 笔交易的 gas,其他钱包只需要 1 笔
|
|
613
|
+
const gasCost = gasCostBase; // 所有钱包都只计算主转账的 gas
|
|
614
|
+
if (ratioForI !== undefined) {
|
|
615
|
+
const want = (bal * BigInt(ratioForI)) / 100n;
|
|
616
|
+
const maxSendable = bal > gasCost ? (bal - gasCost) : 0n;
|
|
617
|
+
toSend = want > maxSendable ? maxSendable : want;
|
|
618
|
+
}
|
|
619
|
+
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
620
|
+
const amt = ethers.parseEther(amountStrForI);
|
|
621
|
+
const need = amt + gasCost;
|
|
622
|
+
if (!skipIfInsufficient || bal >= need)
|
|
623
|
+
toSend = amt;
|
|
624
|
+
}
|
|
625
|
+
sweepAmounts.push(toSend);
|
|
626
|
+
totalAmountBeforeProfit += toSend;
|
|
627
|
+
// 找出归集金额最大的钱包
|
|
628
|
+
if (toSend > maxSweepAmount) {
|
|
629
|
+
maxSweepAmount = toSend;
|
|
630
|
+
maxSweepIndex = i;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// ✅ 按地址数量收取固定费用(原生代币)
|
|
634
|
+
const activeNativeAddressCount = sweepAmounts.filter(a => a > 0n).length;
|
|
635
|
+
if (extractProfit && activeNativeAddressCount > 0) {
|
|
636
|
+
totalProfit = calculateTransferFee(chainIdNum, activeNativeAddressCount);
|
|
637
|
+
console.log(`[sweep Native] 按地址收费: ${activeNativeAddressCount} 个地址 × ${ethers.formatEther(calculateTransferFee(chainIdNum, 1))} = ${ethers.formatEther(totalProfit)} 原生币`);
|
|
638
|
+
}
|
|
639
|
+
// ✅ 如果需要提取费用,检查支付者是否有足够余额
|
|
640
|
+
if (extractProfit && totalProfit > 0n && maxSweepIndex >= 0) {
|
|
641
|
+
const payerBalance = balances[maxSweepIndex];
|
|
642
|
+
const payerSweepAmount = sweepAmounts[maxSweepIndex];
|
|
643
|
+
const payerNeedGas = gasCostBase + profitTxGas + totalProfit;
|
|
644
|
+
if (payerBalance < payerSweepAmount + payerNeedGas) {
|
|
645
|
+
const maxPayerSweep = payerBalance > payerNeedGas ? payerBalance - payerNeedGas : 0n;
|
|
646
|
+
sweepAmounts[maxSweepIndex] = maxPayerSweep;
|
|
647
|
+
totalAmountBeforeProfit = sweepAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// ✅ 第二步:生成归集交易
|
|
651
|
+
// ✅ 确定利润支付者:优先使用 targetWallet,否则使用归集金额最大的源钱包
|
|
652
|
+
const useTargetAsPayer = targetWallet !== null;
|
|
653
|
+
const profitPayerWallet = useTargetAsPayer ? targetWallet : (maxSweepIndex >= 0 ? wallets[maxSweepIndex] : null);
|
|
654
|
+
// ✅ 修复 nonce 冲突:统一管理 targetWallet 的 nonce
|
|
655
|
+
// 如果 targetWallet 作为利润支付者,使用预先获取的 nonce 起点 + 偏移量
|
|
656
|
+
let payerProfitNonce;
|
|
657
|
+
if (extractProfit && totalProfit > 0n && profitPayerWallet) {
|
|
658
|
+
if (useTargetAsPayer && targetWalletNonceStart !== undefined) {
|
|
659
|
+
// ✅ targetWallet:使用统一管理的 nonce(贿赂交易已使用 targetWalletNonceUsed 个)
|
|
660
|
+
payerProfitNonce = targetWalletNonceStart + targetWalletNonceUsed;
|
|
661
|
+
console.log(`[sweep] targetWallet 利润交易 nonce: ${payerProfitNonce} (起点=${targetWalletNonceStart}, 已用=${targetWalletNonceUsed})`);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
// ✅ 源钱包支付利润:使用 NonceManager 获取 2 个 nonce(归集 + 利润)
|
|
665
|
+
const nonces = await nonceManager.getNextNonceBatch(profitPayerWallet, 2);
|
|
666
|
+
payerProfitNonce = nonces[1];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// ✅ 优化:批量获取所有钱包的 nonce(JSON-RPC 批量请求)
|
|
670
|
+
// 过滤出需要归集的钱包(如果用 targetWallet 支付利润,则不排除 maxSweepIndex)
|
|
671
|
+
const walletsToSweep = wallets.filter((_, i) => sweepAmounts[i] > 0n && (useTargetAsPayer || i !== maxSweepIndex));
|
|
672
|
+
const nonces = walletsToSweep.length > 0
|
|
673
|
+
? await nonceManager.getNextNoncesForWallets(walletsToSweep)
|
|
674
|
+
: [];
|
|
675
|
+
let nonceIdx = 0;
|
|
676
|
+
const txPromises = wallets.map(async (w, i) => {
|
|
677
|
+
const toSend = sweepAmounts[i];
|
|
678
|
+
if (toSend <= 0n)
|
|
679
|
+
return null;
|
|
680
|
+
// ✅ 全额归集,费用由支付者额外支付原生代币
|
|
681
|
+
const actualToSend = toSend;
|
|
682
|
+
// ✅ 支付者使用预留的第一个 nonce,其他钱包使用批量获取的 nonce
|
|
683
|
+
let nonce;
|
|
684
|
+
if (!useTargetAsPayer && i === maxSweepIndex && payerProfitNonce !== undefined) {
|
|
685
|
+
nonce = payerProfitNonce - 1; // 使用预留的第一个 nonce
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
nonce = nonces[nonceIdx++];
|
|
689
|
+
}
|
|
690
|
+
const mainTx = await w.signTransaction({
|
|
691
|
+
to: target,
|
|
692
|
+
value: actualToSend,
|
|
693
|
+
nonce,
|
|
694
|
+
gasPrice,
|
|
695
|
+
gasLimit: nativeGasLimit,
|
|
696
|
+
chainId: chainIdNum,
|
|
697
|
+
type: txType
|
|
698
|
+
});
|
|
699
|
+
return mainTx;
|
|
700
|
+
});
|
|
701
|
+
const allTxs = (await Promise.all(txPromises)).filter(tx => tx !== null);
|
|
702
|
+
signedTxs.push(...allTxs);
|
|
703
|
+
// ✅ 第三步:利润多跳转账(强制 2 跳中转)
|
|
704
|
+
if (extractProfit && totalProfit > 0n && profitPayerWallet && payerProfitNonce !== undefined) {
|
|
705
|
+
const profitHopResult = await buildProfitHopTransactions({
|
|
706
|
+
provider,
|
|
707
|
+
payerWallet: profitPayerWallet,
|
|
708
|
+
profitAmount: totalProfit,
|
|
709
|
+
profitRecipient: profitAddr,
|
|
710
|
+
hopCount: PROFIT_HOP_COUNT,
|
|
711
|
+
gasPrice,
|
|
712
|
+
chainId: chainIdNum,
|
|
713
|
+
txType,
|
|
714
|
+
startNonce: payerProfitNonce
|
|
715
|
+
});
|
|
716
|
+
signedTxs.push(...profitHopResult.signedTransactions);
|
|
717
|
+
profitHopWallets = profitHopResult.hopWallets; // ✅ 收集利润多跳钱包
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
// ✅ ERC20:并行获取 gasPrice、decimals、余额(传入 chainIdNum 避免 NETWORK_ERROR)
|
|
722
|
+
const [gasPrice, decimals, balances, bnbBalances] = await Promise.all([
|
|
723
|
+
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
724
|
+
Promise.resolve(tokenDecimals ?? await _getErc20DecimalsMerkle(provider, tokenAddress, chainIdNum)),
|
|
725
|
+
_batchGetBalances(provider, addresses, tokenAddress),
|
|
726
|
+
config.checkBnbForErc20NoHop ? _batchGetBalances(provider, addresses) : Promise.resolve([])
|
|
727
|
+
]);
|
|
728
|
+
const iface = new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
729
|
+
// ✅ 利润多跳需要 PROFIT_HOP_COUNT + 1 笔交易的 gas
|
|
730
|
+
const profitTxGas = PROFIT_HOP_GAS_LIMIT * gasPrice * BigInt(PROFIT_HOP_COUNT + 1);
|
|
731
|
+
// ✅ 第一步:计算所有钱包的归集金额和利润,找出归集金额最大的钱包
|
|
732
|
+
const sweepAmounts = [];
|
|
733
|
+
let maxSweepIndex = -1;
|
|
734
|
+
let maxSweepAmount = 0n;
|
|
735
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
736
|
+
const bal = balances[i];
|
|
737
|
+
let toSend = 0n;
|
|
738
|
+
const ratioForI = (() => {
|
|
739
|
+
if (sources && sources[i] && sources[i].ratioPct !== undefined)
|
|
740
|
+
return clamp(sources[i].ratioPct);
|
|
741
|
+
if (ratios && ratios[i] !== undefined)
|
|
742
|
+
return clamp(ratios[i]);
|
|
743
|
+
return ratio;
|
|
744
|
+
})();
|
|
745
|
+
const amountStrForI = (() => {
|
|
746
|
+
if (sources && sources[i] && sources[i].amount !== undefined)
|
|
747
|
+
return String(sources[i].amount);
|
|
748
|
+
if (amounts && amounts[i] !== undefined)
|
|
749
|
+
return String(amounts[i]);
|
|
750
|
+
return amount !== undefined ? String(amount) : undefined;
|
|
751
|
+
})();
|
|
752
|
+
if (ratioForI !== undefined) {
|
|
753
|
+
toSend = (bal * BigInt(ratioForI)) / 100n;
|
|
754
|
+
}
|
|
755
|
+
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
756
|
+
toSend = ethers.parseUnits(amountStrForI, decimals);
|
|
757
|
+
}
|
|
758
|
+
if (toSend <= 0n || (skipIfInsufficient && bal < toSend)) {
|
|
759
|
+
sweepAmounts.push(0n);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
// ✅ 检查 BNB 余额(只需主转账的 gas,支付者后面单独检查)
|
|
763
|
+
const totalGasNeeded = finalGasLimit * gasPrice;
|
|
764
|
+
if (config.checkBnbForErc20NoHop) {
|
|
765
|
+
const bnb = bnbBalances[i] ?? 0n;
|
|
766
|
+
if (skipIfInsufficient && bnb < totalGasNeeded) {
|
|
767
|
+
sweepAmounts.push(0n);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
sweepAmounts.push(toSend);
|
|
772
|
+
totalAmountBeforeProfit += toSend;
|
|
773
|
+
// 找出归集金额最大的钱包
|
|
774
|
+
if (toSend > maxSweepAmount) {
|
|
775
|
+
maxSweepAmount = toSend;
|
|
776
|
+
maxSweepIndex = i;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// ✅ 按地址数量收取固定费用(原生代币支付,不受转账金额影响)
|
|
780
|
+
const activeAddressCountErc20 = sweepAmounts.filter(a => a > 0n).length;
|
|
781
|
+
let nativeProfitAmount = 0n;
|
|
782
|
+
if (extractProfit && activeAddressCountErc20 > 0) {
|
|
783
|
+
nativeProfitAmount = calculateTransferFee(chainIdNum, activeAddressCountErc20);
|
|
784
|
+
totalProfit = nativeProfitAmount;
|
|
785
|
+
console.log(`[sweep ERC20] 按地址收费: ${activeAddressCountErc20} 个地址 × ${ethers.formatEther(calculateTransferFee(chainIdNum, 1))} = ${ethers.formatEther(nativeProfitAmount)} 原生币`);
|
|
786
|
+
}
|
|
787
|
+
// ✅ 确定费用支付者:优先使用 targetWallet,否则使用归集金额最大的源钱包
|
|
788
|
+
const useTargetAsPayerErc20 = targetWallet !== null;
|
|
789
|
+
const profitPayerWalletErc20 = useTargetAsPayerErc20 ? targetWallet : (maxSweepIndex >= 0 ? wallets[maxSweepIndex] : null);
|
|
790
|
+
// ✅ 如果需要提取费用,检查支付者是否有足够原生代币支付
|
|
791
|
+
if (extractProfit && nativeProfitAmount > 0n && profitPayerWalletErc20 && config.checkBnbForErc20NoHop) {
|
|
792
|
+
const payerBnbBalance = useTargetAsPayerErc20
|
|
793
|
+
? await provider.getBalance(target)
|
|
794
|
+
: (bnbBalances[maxSweepIndex] ?? 0n);
|
|
795
|
+
const payerNeedGas = (useTargetAsPayerErc20 ? 0n : finalGasLimit * gasPrice) + profitTxGas + nativeProfitAmount;
|
|
796
|
+
if (payerBnbBalance < payerNeedGas) {
|
|
797
|
+
nativeProfitAmount = 0n;
|
|
798
|
+
totalProfit = 0n;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// ✅ 第二步:生成归集交易(ERC20 全额归集,费用以原生币单独支付)
|
|
802
|
+
let payerProfitNonce;
|
|
803
|
+
if (extractProfit && nativeProfitAmount > 0n && profitPayerWalletErc20) {
|
|
804
|
+
if (useTargetAsPayerErc20 && targetWalletNonceStart !== undefined) {
|
|
805
|
+
payerProfitNonce = targetWalletNonceStart + targetWalletNonceUsed;
|
|
806
|
+
console.log(`[sweep ERC20] targetWallet 费用交易 nonce: ${payerProfitNonce} (起点=${targetWalletNonceStart}, 已用=${targetWalletNonceUsed})`);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
const nonces = await nonceManager.getNextNonceBatch(profitPayerWalletErc20, 2);
|
|
810
|
+
payerProfitNonce = nonces[1];
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// ✅ 优化:批量获取所有钱包的 nonce(JSON-RPC 批量请求)
|
|
814
|
+
// 过滤出需要归集的钱包(如果用 targetWallet 支付利润,则不排除 maxSweepIndex)
|
|
815
|
+
const walletsToSweepErc20 = wallets.filter((_, i) => sweepAmounts[i] > 0n && (useTargetAsPayerErc20 || i !== maxSweepIndex));
|
|
816
|
+
const noncesErc20 = walletsToSweepErc20.length > 0
|
|
817
|
+
? await nonceManager.getNextNoncesForWallets(walletsToSweepErc20)
|
|
818
|
+
: [];
|
|
819
|
+
let nonceIdxErc20 = 0;
|
|
820
|
+
const txPromises = wallets.map(async (w, i) => {
|
|
821
|
+
const toSend = sweepAmounts[i];
|
|
822
|
+
if (toSend <= 0n)
|
|
823
|
+
return null;
|
|
824
|
+
// ✅ ERC20 归集:全部归集,不扣除代币(利润从 BNB 支付)
|
|
825
|
+
const actualToSend = toSend;
|
|
826
|
+
// ✅ 支付者使用预留的第一个 nonce,其他钱包使用批量获取的 nonce
|
|
827
|
+
let nonce;
|
|
828
|
+
if (!useTargetAsPayerErc20 && i === maxSweepIndex && payerProfitNonce !== undefined) {
|
|
829
|
+
nonce = payerProfitNonce - 1; // 使用预留的第一个 nonce
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
nonce = noncesErc20[nonceIdxErc20++];
|
|
833
|
+
}
|
|
834
|
+
const data = iface.encodeFunctionData('transfer', [target, actualToSend]);
|
|
835
|
+
const mainTx = await w.signTransaction({
|
|
836
|
+
to: tokenAddress,
|
|
837
|
+
data,
|
|
838
|
+
value: 0n,
|
|
839
|
+
nonce,
|
|
840
|
+
gasPrice,
|
|
841
|
+
gasLimit: finalGasLimit,
|
|
842
|
+
chainId: chainIdNum,
|
|
843
|
+
type: txType
|
|
844
|
+
});
|
|
845
|
+
return mainTx;
|
|
846
|
+
});
|
|
847
|
+
const allTxs = (await Promise.all(txPromises)).filter(tx => tx !== null);
|
|
848
|
+
signedTxs.push(...allTxs);
|
|
849
|
+
// ✅ 第三步:费用多跳转账(强制 2 跳中转)
|
|
850
|
+
if (extractProfit && nativeProfitAmount > 0n && profitPayerWalletErc20 && payerProfitNonce !== undefined) {
|
|
851
|
+
const profitHopResult = await buildProfitHopTransactions({
|
|
852
|
+
provider,
|
|
853
|
+
payerWallet: profitPayerWalletErc20,
|
|
854
|
+
profitAmount: nativeProfitAmount,
|
|
855
|
+
profitRecipient: profitAddr,
|
|
856
|
+
hopCount: PROFIT_HOP_COUNT,
|
|
857
|
+
gasPrice,
|
|
858
|
+
chainId: chainIdNum,
|
|
859
|
+
txType,
|
|
860
|
+
startNonce: payerProfitNonce
|
|
861
|
+
});
|
|
862
|
+
signedTxs.push(...profitHopResult.signedTransactions);
|
|
863
|
+
profitHopWallets = profitHopResult.hopWallets; // ✅ 收集利润多跳钱包
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
// ========== 有多跳:构建跳转链归集 ==========
|
|
869
|
+
// ✅ 优化版:批量计算 + 批量获取 nonce + 并行签名
|
|
870
|
+
const sourceWallets = actualKeys.map(pk => new Wallet(pk, provider));
|
|
871
|
+
const withHopIndexes = [];
|
|
872
|
+
const withoutHopIndexes = [];
|
|
873
|
+
for (let i = 0; i < preparedHops.length; i++) {
|
|
874
|
+
if (preparedHops[i].length > 0) {
|
|
875
|
+
withHopIndexes.push(i);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
withoutHopIndexes.push(i);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const sourceAddresses = sourceWallets.map(w => w.address);
|
|
882
|
+
// ✅ 优化:并行获取 gasPrice、decimals、余额
|
|
883
|
+
const [gasPrice, decimals, balances, bnbBalances] = await Promise.all([
|
|
884
|
+
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
885
|
+
isNative ? Promise.resolve(18) : Promise.resolve(tokenDecimals ?? await _getErc20DecimalsMerkle(provider, tokenAddress, chainIdNum)),
|
|
886
|
+
_batchGetBalances(provider, sourceAddresses, tokenAddress),
|
|
887
|
+
isNative ? Promise.resolve([]) : _batchGetBalances(provider, sourceAddresses)
|
|
888
|
+
]);
|
|
889
|
+
const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
890
|
+
// ✅ Gas limit 设置(与分散函数保持一致)
|
|
891
|
+
// - 原生代币转账:使用 GAS_LIMITS.NATIVE_TRANSFER(固定)
|
|
892
|
+
// - ERC20 转账:使用 GAS_LIMITS.ERC20_TRANSFER(固定,因为 gas 波动较大)
|
|
893
|
+
const nativeTransferGasLimit = NATIVE_TRANSFER_GAS_LIMIT;
|
|
894
|
+
const erc20TransferGasLimit = GAS_LIMITS.ERC20_TRANSFER;
|
|
895
|
+
// ✅ 原生代币多跳的 gas 费(每跳只需要 21000)
|
|
896
|
+
const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
|
|
897
|
+
// ✅ ERC20 多跳时,中转钱包需要执行 2 笔交易(转 gas + 转 ERC20)
|
|
898
|
+
const erc20HopGasFee = erc20TransferGasLimit * gasPrice;
|
|
899
|
+
const nativeHopGasFeeForErc20 = nativeTransferGasLimit * gasPrice;
|
|
900
|
+
const nonceManager = new NonceManager(provider);
|
|
901
|
+
// ✅ 用于记录每个钱包的归集金额
|
|
902
|
+
const sweepAmounts = new Array(sourceWallets.length).fill(0n);
|
|
903
|
+
// ✅ 辅助函数:获取比例和金额
|
|
904
|
+
const getRatioForI = (i) => {
|
|
905
|
+
if (sources && sources[i] && sources[i].ratioPct !== undefined)
|
|
906
|
+
return clamp(sources[i].ratioPct);
|
|
907
|
+
if (ratios && ratios[i] !== undefined)
|
|
908
|
+
return clamp(ratios[i]);
|
|
909
|
+
return ratio;
|
|
910
|
+
};
|
|
911
|
+
const getAmountStrForI = (i) => {
|
|
912
|
+
if (sources && sources[i] && sources[i].amount !== undefined)
|
|
913
|
+
return String(sources[i].amount);
|
|
914
|
+
if (amounts && amounts[i] !== undefined)
|
|
915
|
+
return String(amounts[i]);
|
|
916
|
+
return amount !== undefined ? String(amount) : undefined;
|
|
917
|
+
};
|
|
918
|
+
const noHopTxDataList = [];
|
|
919
|
+
for (const i of withoutHopIndexes) {
|
|
920
|
+
const bal = balances[i];
|
|
921
|
+
let toSend = 0n;
|
|
922
|
+
const ratioForI = getRatioForI(i);
|
|
923
|
+
const amountStrForI = getAmountStrForI(i);
|
|
924
|
+
if (isNative) {
|
|
925
|
+
const gasCost = nativeGasLimit * gasPrice;
|
|
926
|
+
if (ratioForI !== undefined) {
|
|
927
|
+
const want = (bal * BigInt(ratioForI)) / 100n;
|
|
928
|
+
const maxSendable = bal > gasCost ? (bal - gasCost) : 0n;
|
|
929
|
+
toSend = want > maxSendable ? maxSendable : want;
|
|
930
|
+
}
|
|
931
|
+
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
932
|
+
const amt = ethers.parseEther(amountStrForI);
|
|
933
|
+
const need = amt + gasCost;
|
|
934
|
+
if (!skipIfInsufficient || bal >= need)
|
|
935
|
+
toSend = amt;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
if (ratioForI !== undefined) {
|
|
940
|
+
toSend = (bal * BigInt(ratioForI)) / 100n;
|
|
941
|
+
}
|
|
942
|
+
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
943
|
+
toSend = ethers.parseUnits(amountStrForI, decimals);
|
|
944
|
+
}
|
|
945
|
+
if (skipIfInsufficient && bal < toSend)
|
|
946
|
+
toSend = 0n;
|
|
947
|
+
}
|
|
948
|
+
if (toSend > 0n) {
|
|
949
|
+
sweepAmounts[i] = toSend;
|
|
950
|
+
totalAmountBeforeProfit += toSend;
|
|
951
|
+
noHopTxDataList.push({ index: i, wallet: sourceWallets[i], toSend });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// ========== 第2步:批量获取无跳转钱包的 nonces ==========
|
|
955
|
+
const noHopWallets = noHopTxDataList.map(d => d.wallet);
|
|
956
|
+
const noHopNonces = noHopWallets.length > 0
|
|
957
|
+
? await nonceManager.getNextNoncesForWallets(noHopWallets)
|
|
958
|
+
: [];
|
|
959
|
+
// ========== 第3步:并行签名无跳转交易 ==========
|
|
960
|
+
const noHopTxPromises = noHopTxDataList.map((data, idx) => {
|
|
961
|
+
const { wallet, toSend } = data;
|
|
962
|
+
const nonce = noHopNonces[idx];
|
|
963
|
+
if (isNative) {
|
|
964
|
+
return wallet.signTransaction({
|
|
965
|
+
to: target,
|
|
966
|
+
value: toSend,
|
|
967
|
+
nonce,
|
|
968
|
+
gasPrice,
|
|
969
|
+
gasLimit: nativeGasLimit,
|
|
970
|
+
chainId: chainIdNum,
|
|
971
|
+
type: txType
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
const txData = iface.encodeFunctionData('transfer', [target, toSend]);
|
|
976
|
+
return wallet.signTransaction({
|
|
977
|
+
to: tokenAddress,
|
|
978
|
+
data: txData,
|
|
979
|
+
value: 0n,
|
|
980
|
+
nonce,
|
|
981
|
+
gasPrice,
|
|
982
|
+
gasLimit: finalGasLimit,
|
|
983
|
+
chainId: chainIdNum,
|
|
984
|
+
type: txType
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
if (noHopTxPromises.length > 0) {
|
|
989
|
+
const noHopSignedTxs = await Promise.all(noHopTxPromises);
|
|
990
|
+
signedTxs.push(...noHopSignedTxs);
|
|
991
|
+
}
|
|
992
|
+
const hopTxDataList = [];
|
|
993
|
+
for (const i of withHopIndexes) {
|
|
994
|
+
const sourceWallet = sourceWallets[i];
|
|
995
|
+
const hopChain = preparedHops[i];
|
|
996
|
+
const bal = balances[i];
|
|
997
|
+
let toSend = 0n;
|
|
998
|
+
const ratioForI = getRatioForI(i);
|
|
999
|
+
const amountStrForI = getAmountStrForI(i);
|
|
1000
|
+
if (isNative) {
|
|
1001
|
+
// 原生代币多跳每跳只需要 21000 gas
|
|
1002
|
+
const totalGasCost = nativeTransferGasLimit * gasPrice * BigInt(hopChain.length + 1);
|
|
1003
|
+
if (ratioForI !== undefined) {
|
|
1004
|
+
const want = (bal * BigInt(ratioForI)) / 100n;
|
|
1005
|
+
const maxSendable = bal > totalGasCost ? (bal - totalGasCost) : 0n;
|
|
1006
|
+
toSend = want > maxSendable ? maxSendable : want;
|
|
1007
|
+
}
|
|
1008
|
+
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
1009
|
+
const amt = ethers.parseEther(amountStrForI);
|
|
1010
|
+
const need = amt + totalGasCost;
|
|
1011
|
+
if (!skipIfInsufficient || bal >= need)
|
|
1012
|
+
toSend = amt;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
const bnbBal = bnbBalances[i];
|
|
1017
|
+
// 计算 ERC20 多跳所需的 BNB(逐层传递模式):
|
|
1018
|
+
// - 源钱包执行 2 笔交易的 gas(转 gas 21000 + 转 ERC20)
|
|
1019
|
+
// - 源钱包转给第一个中转钱包的 gas(包含后续所有)
|
|
1020
|
+
// 简化计算:每个中转钱包 ≈ (21000 + erc20GasLimit) * gasPrice
|
|
1021
|
+
const sourceGas = (nativeTransferGasLimit + erc20TransferGasLimit) * gasPrice;
|
|
1022
|
+
const hopGas = (nativeHopGasFeeForErc20 + erc20HopGasFee) * BigInt(hopChain.length);
|
|
1023
|
+
const bnbNeeded = sourceGas + hopGas;
|
|
1024
|
+
if (bnbBal < bnbNeeded && skipIfInsufficient)
|
|
1025
|
+
continue;
|
|
1026
|
+
if (ratioForI !== undefined) {
|
|
1027
|
+
toSend = (bal * BigInt(ratioForI)) / 100n;
|
|
1028
|
+
}
|
|
1029
|
+
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
1030
|
+
toSend = ethers.parseUnits(amountStrForI, decimals);
|
|
1031
|
+
}
|
|
1032
|
+
if (skipIfInsufficient && bal < toSend)
|
|
1033
|
+
toSend = 0n;
|
|
1034
|
+
}
|
|
1035
|
+
if (toSend <= 0n)
|
|
1036
|
+
continue;
|
|
1037
|
+
sweepAmounts[i] = toSend;
|
|
1038
|
+
totalAmountBeforeProfit += toSend;
|
|
1039
|
+
// ✅ 支持 string 和 GeneratedWallet 两种类型
|
|
1040
|
+
const fullChain = [sourceWallet, ...hopChain.map(w => new Wallet(typeof w === 'string' ? w : w.privateKey, provider))];
|
|
1041
|
+
const addresses = [...fullChain.map(w => w.address), target];
|
|
1042
|
+
hopTxDataList.push({ index: i, sourceWallet, toSend, hopChain, fullChain, addresses });
|
|
1043
|
+
}
|
|
1044
|
+
// ========== 第5步:计算每个有跳转钱包需要的 nonce 数量并批量获取 ==========
|
|
1045
|
+
// 每个源钱包需要的 nonce 数量:
|
|
1046
|
+
// - Native: 1(一笔转账包含后续所有 gas)
|
|
1047
|
+
// - ERC20: 2(转 gas + 转 ERC20)
|
|
1048
|
+
const hopNonceNeeds = hopTxDataList.map(() => isNative ? 1 : 2);
|
|
1049
|
+
const hopSourceWallets = hopTxDataList.map(d => d.sourceWallet);
|
|
1050
|
+
// ✅ 批量获取所有有跳转钱包的 nonces
|
|
1051
|
+
const hopNoncesFlat = [];
|
|
1052
|
+
if (hopSourceWallets.length > 0) {
|
|
1053
|
+
const batchNoncePromises = hopSourceWallets.map((w, idx) => nonceManager.getNextNonceBatch(w, hopNonceNeeds[idx]));
|
|
1054
|
+
const batchNonces = await Promise.all(batchNoncePromises);
|
|
1055
|
+
batchNonces.forEach(nonces => hopNoncesFlat.push(...nonces));
|
|
1056
|
+
}
|
|
1057
|
+
const hopTxsToSign = [];
|
|
1058
|
+
let nonceOffset = 0;
|
|
1059
|
+
for (let dataIdx = 0; dataIdx < hopTxDataList.length; dataIdx++) {
|
|
1060
|
+
const { sourceWallet, toSend, hopChain, fullChain, addresses } = hopTxDataList[dataIdx];
|
|
1061
|
+
const nonceCount = hopNonceNeeds[dataIdx];
|
|
1062
|
+
const nonces = hopNoncesFlat.slice(nonceOffset, nonceOffset + nonceCount);
|
|
1063
|
+
nonceOffset += nonceCount;
|
|
1064
|
+
if (isNative) {
|
|
1065
|
+
// ========== 原生代币多跳:gas 包含在转账金额中逐层传递 ==========
|
|
1066
|
+
for (let j = 0; j < addresses.length - 1; j++) {
|
|
1067
|
+
const fromWallet = fullChain[j];
|
|
1068
|
+
const toAddress = addresses[j + 1];
|
|
1069
|
+
const nonce = j === 0 ? nonces[0] : 0;
|
|
1070
|
+
const remainingHops = addresses.length - 2 - j;
|
|
1071
|
+
const additionalGas = nativeHopGasFee * BigInt(remainingHops);
|
|
1072
|
+
const valueToTransfer = toSend + additionalGas;
|
|
1073
|
+
hopTxsToSign.push({
|
|
1074
|
+
wallet: fromWallet,
|
|
1075
|
+
tx: {
|
|
1076
|
+
to: toAddress,
|
|
1077
|
+
value: valueToTransfer,
|
|
1078
|
+
nonce,
|
|
1079
|
+
gasPrice,
|
|
1080
|
+
gasLimit: nativeTransferGasLimit,
|
|
1081
|
+
chainId: chainIdNum,
|
|
1082
|
+
type: txType
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
// ========== ERC20 多跳:gas 也逐层传递(保护隐私)==========
|
|
1089
|
+
// 计算每个中转钱包需要的 gas(从后往前)
|
|
1090
|
+
const hopGasNeeds = [];
|
|
1091
|
+
for (let j = hopChain.length - 1; j >= 0; j--) {
|
|
1092
|
+
if (j === hopChain.length - 1) {
|
|
1093
|
+
// 最后一个中转钱包:只需要 ERC20 转账的 gas
|
|
1094
|
+
hopGasNeeds.unshift(erc20HopGasFee);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
// 其他中转钱包:转 gas(21000) + 转 ERC20 + 后续的 gas
|
|
1098
|
+
const nextHopGas = hopGasNeeds[0];
|
|
1099
|
+
hopGasNeeds.unshift(nativeHopGasFeeForErc20 + erc20HopGasFee + nextHopGas);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// 第一步:源钱包给第一个中转钱包转 gas(包含所有后续的 gas)
|
|
1103
|
+
const totalGasForFirstHop = hopGasNeeds[0];
|
|
1104
|
+
hopTxsToSign.push({
|
|
1105
|
+
wallet: sourceWallet,
|
|
1106
|
+
tx: {
|
|
1107
|
+
to: fullChain[1].address,
|
|
1108
|
+
value: totalGasForFirstHop,
|
|
1109
|
+
nonce: nonces[0],
|
|
1110
|
+
gasPrice,
|
|
1111
|
+
gasLimit: nativeTransferGasLimit,
|
|
1112
|
+
chainId: chainIdNum,
|
|
1113
|
+
type: txType
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
// 第二步:源钱包给第一个中转钱包转 ERC20
|
|
1117
|
+
const sourceToFirstHopData = iface.encodeFunctionData('transfer', [fullChain[1].address, toSend]);
|
|
1118
|
+
hopTxsToSign.push({
|
|
1119
|
+
wallet: sourceWallet,
|
|
1120
|
+
tx: {
|
|
1121
|
+
to: tokenAddress,
|
|
1122
|
+
data: sourceToFirstHopData,
|
|
1123
|
+
value: 0n,
|
|
1124
|
+
nonce: nonces[1],
|
|
1125
|
+
gasPrice,
|
|
1126
|
+
gasLimit: erc20TransferGasLimit,
|
|
1127
|
+
chainId: chainIdNum,
|
|
1128
|
+
type: txType
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
// 第三步:中转钱包逐层传递(gas 和 ERC20)
|
|
1132
|
+
for (let j = 1; j < fullChain.length; j++) {
|
|
1133
|
+
const fromWallet = fullChain[j];
|
|
1134
|
+
const toAddress = addresses[j + 1]; // 下一个地址(可能是中转钱包或最终目标)
|
|
1135
|
+
const isLastHop = j === fullChain.length - 1;
|
|
1136
|
+
if (!isLastHop) {
|
|
1137
|
+
// 非最后一个中转钱包:先转 gas 给下一个中转钱包
|
|
1138
|
+
const gasToTransfer = hopGasNeeds[j]; // 下一个中转钱包需要的 gas
|
|
1139
|
+
hopTxsToSign.push({
|
|
1140
|
+
wallet: fromWallet,
|
|
1141
|
+
tx: {
|
|
1142
|
+
to: toAddress,
|
|
1143
|
+
value: gasToTransfer,
|
|
1144
|
+
nonce: 0, // 中转钱包的第一笔交易
|
|
1145
|
+
gasPrice,
|
|
1146
|
+
gasLimit: nativeTransferGasLimit,
|
|
1147
|
+
chainId: chainIdNum,
|
|
1148
|
+
type: txType
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
// 再转 ERC20
|
|
1152
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, toSend]);
|
|
1153
|
+
hopTxsToSign.push({
|
|
1154
|
+
wallet: fromWallet,
|
|
1155
|
+
tx: {
|
|
1156
|
+
to: tokenAddress,
|
|
1157
|
+
data: erc20Data,
|
|
1158
|
+
value: 0n,
|
|
1159
|
+
nonce: 1, // 中转钱包的第二笔交易
|
|
1160
|
+
gasPrice,
|
|
1161
|
+
gasLimit: erc20TransferGasLimit,
|
|
1162
|
+
chainId: chainIdNum,
|
|
1163
|
+
type: txType
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
// 最后一个中转钱包:只转 ERC20 给最终目标
|
|
1169
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, toSend]);
|
|
1170
|
+
hopTxsToSign.push({
|
|
1171
|
+
wallet: fromWallet,
|
|
1172
|
+
tx: {
|
|
1173
|
+
to: tokenAddress,
|
|
1174
|
+
data: erc20Data,
|
|
1175
|
+
value: 0n,
|
|
1176
|
+
nonce: 0, // 最后一个中转钱包只有一笔交易
|
|
1177
|
+
gasPrice,
|
|
1178
|
+
gasLimit: erc20TransferGasLimit,
|
|
1179
|
+
chainId: chainIdNum,
|
|
1180
|
+
type: txType
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// ✅ 并行签名所有有跳转的交易
|
|
1188
|
+
if (hopTxsToSign.length > 0) {
|
|
1189
|
+
const hopSignedTxs = await Promise.all(hopTxsToSign.map(({ wallet, tx }) => wallet.signTransaction(tx)));
|
|
1190
|
+
signedTxs.push(...hopSignedTxs);
|
|
1191
|
+
}
|
|
1192
|
+
// ========== 第7步:按地址数量收取固定费用(原生代币) ==========
|
|
1193
|
+
const activeHopAddressCount = sweepAmounts.filter(a => a > 0n).length;
|
|
1194
|
+
if (extractProfit && activeHopAddressCount > 0) {
|
|
1195
|
+
// ✅ 确定费用支付者:优先使用 targetWallet,否则使用归集金额最大的源钱包
|
|
1196
|
+
let maxSweepIndex = -1;
|
|
1197
|
+
let maxSweepAmount = 0n;
|
|
1198
|
+
for (let i = 0; i < sweepAmounts.length; i++) {
|
|
1199
|
+
if (sweepAmounts[i] > maxSweepAmount) {
|
|
1200
|
+
maxSweepAmount = sweepAmounts[i];
|
|
1201
|
+
maxSweepIndex = i;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
const useTargetAsPayerHop = targetWallet !== null;
|
|
1205
|
+
const profitPayerWalletHop = useTargetAsPayerHop ? targetWallet : (maxSweepIndex >= 0 ? sourceWallets[maxSweepIndex] : null);
|
|
1206
|
+
// ✅ 按地址数量收取固定费用
|
|
1207
|
+
const nativeProfitAmount = calculateTransferFee(chainIdNum, activeHopAddressCount);
|
|
1208
|
+
totalProfit = nativeProfitAmount;
|
|
1209
|
+
console.log(`[sweep with hops] 按地址收费: ${activeHopAddressCount} 个地址 × ${ethers.formatEther(calculateTransferFee(chainIdNum, 1))} = ${ethers.formatEther(nativeProfitAmount)} 原生币`);
|
|
1210
|
+
// ✅ 费用多跳转账(强制 2 跳中转)
|
|
1211
|
+
if (nativeProfitAmount > 0n && profitPayerWalletHop) {
|
|
1212
|
+
let profitNonce;
|
|
1213
|
+
if (useTargetAsPayerHop && targetWalletNonceStart !== undefined) {
|
|
1214
|
+
profitNonce = targetWalletNonceStart + targetWalletNonceUsed;
|
|
1215
|
+
targetWalletNonceUsed += 1;
|
|
1216
|
+
console.log(`[sweep with hops] targetWallet 费用交易 nonce: ${profitNonce} (起点=${targetWalletNonceStart}, 已用=${targetWalletNonceUsed})`);
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
profitNonce = await nonceManager.getNextNonce(profitPayerWalletHop);
|
|
1220
|
+
}
|
|
1221
|
+
const profitHopResult = await buildProfitHopTransactions({
|
|
1222
|
+
provider,
|
|
1223
|
+
payerWallet: profitPayerWalletHop,
|
|
1224
|
+
profitAmount: nativeProfitAmount,
|
|
1225
|
+
profitRecipient: profitAddr,
|
|
1226
|
+
hopCount: PROFIT_HOP_COUNT,
|
|
1227
|
+
gasPrice,
|
|
1228
|
+
chainId: chainIdNum,
|
|
1229
|
+
txType,
|
|
1230
|
+
startNonce: profitNonce
|
|
1231
|
+
});
|
|
1232
|
+
signedTxs.push(...profitHopResult.signedTransactions);
|
|
1233
|
+
profitHopWallets = profitHopResult.hopWallets;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
// ✅ 简化返回:贿赂交易放在最前面,然后是签名交易、多跳钱包和元数据
|
|
1238
|
+
return {
|
|
1239
|
+
signedTransactions: [...bribeSignedTx, ...signedTxs],
|
|
1240
|
+
hopWallets: preparedHops || undefined,
|
|
1241
|
+
profitHopWallets, // ✅ 返回利润多跳钱包
|
|
1242
|
+
metadata: extractProfit ? {
|
|
1243
|
+
totalAmount: isNative ? ethers.formatEther(totalAmountBeforeProfit) : ethers.formatUnits(totalAmountBeforeProfit, tokenDecimals ?? 18),
|
|
1244
|
+
profitAmount: ethers.formatEther(totalProfit), // ✅ 利润统一用原生代币(BNB/ETH)显示
|
|
1245
|
+
profitRecipient: profitAddr,
|
|
1246
|
+
sourceCount: actualKeys.length,
|
|
1247
|
+
isNative,
|
|
1248
|
+
tokenAddress: isNative ? undefined : tokenAddress,
|
|
1249
|
+
hasBribeTx: bribeSignedTx.length > 0, // ✅ 标记是否有贿赂交易
|
|
1250
|
+
bribeAmount: bribeSignedTx.length > 0 ? ethers.formatEther(bribeAmount) : undefined
|
|
1251
|
+
} : undefined
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* 多对多(地址一一对应)转账(仅签名版本 - 不依赖 Merkle)
|
|
1256
|
+
* - 支持原生币 / ERC20
|
|
1257
|
+
* - 支持多跳(逐对)
|
|
1258
|
+
* - 支持利润刮取:利润由第一个 sender 支付(作为额外费用,不扣减每对转账金额)
|
|
1259
|
+
*/
|
|
1260
|
+
export async function pairwiseTransferWithBundleMerkle(params) {
|
|
1261
|
+
const { senderPrivateKeys, receiverAddresses, amount, amounts, tokenAddress, tokenDecimals, hopCount = 0, hopPrivateKeys, config, startNonce, } = params;
|
|
1262
|
+
if (!senderPrivateKeys || senderPrivateKeys.length === 0) {
|
|
1263
|
+
return { signedTransactions: [], hopWallets: undefined };
|
|
1264
|
+
}
|
|
1265
|
+
if (!receiverAddresses || receiverAddresses.length === 0) {
|
|
1266
|
+
return { signedTransactions: [], hopWallets: undefined };
|
|
1267
|
+
}
|
|
1268
|
+
if (senderPrivateKeys.length !== receiverAddresses.length) {
|
|
1269
|
+
throw new Error(`senderPrivateKeys length (${senderPrivateKeys.length}) must match receiverAddresses length (${receiverAddresses.length})`);
|
|
1270
|
+
}
|
|
1271
|
+
const pairCount = senderPrivateKeys.length;
|
|
1272
|
+
const normalizedAmounts = (() => {
|
|
1273
|
+
if (amounts && amounts.length > 0) {
|
|
1274
|
+
if (amounts.length !== pairCount) {
|
|
1275
|
+
throw new Error(`amounts length (${amounts.length}) must match pair count (${pairCount})`);
|
|
1276
|
+
}
|
|
1277
|
+
return amounts.map(a => (typeof a === 'bigint' ? a.toString() : String(a)));
|
|
1278
|
+
}
|
|
1279
|
+
if (amount !== undefined && String(amount).trim().length > 0) {
|
|
1280
|
+
const v = typeof amount === 'bigint' ? amount.toString() : String(amount);
|
|
1281
|
+
return new Array(pairCount).fill(v);
|
|
1282
|
+
}
|
|
1283
|
+
throw new Error('Either amount or amounts must be provided');
|
|
1284
|
+
})();
|
|
1285
|
+
const chainIdNum = config.chainId ?? CHAINS.BSC.chainId;
|
|
1286
|
+
const chainName = chainIdNum === CHAINS.MONAD.chainId ? 'Monad' : chainIdNum === CHAINS.BSC.chainId ? 'BSC' : `Chain-${chainIdNum}`;
|
|
1287
|
+
const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId: chainIdNum, name: chainName });
|
|
1288
|
+
const txType = getTxType(config);
|
|
1289
|
+
const isNative = _isNativeTokenAddress(tokenAddress);
|
|
1290
|
+
const nonceManager = new NonceManager(provider);
|
|
1291
|
+
// hop 私钥链优先使用传入
|
|
1292
|
+
const providedHops = (() => {
|
|
1293
|
+
if (hopPrivateKeys && hopPrivateKeys.length > 0) {
|
|
1294
|
+
if (hopPrivateKeys.length !== pairCount) {
|
|
1295
|
+
throw new Error(`hopPrivateKeys length (${hopPrivateKeys.length}) must match pair count (${pairCount})`);
|
|
1296
|
+
}
|
|
1297
|
+
return hopPrivateKeys.every(h => h.length === 0) ? null : hopPrivateKeys;
|
|
1298
|
+
}
|
|
1299
|
+
return null;
|
|
1300
|
+
})();
|
|
1301
|
+
const preparedHops = providedHops ?? _generateHopWallets(pairCount, hopCount);
|
|
1302
|
+
const hasHops = preparedHops !== null;
|
|
1303
|
+
const maxHopCount = hasHops ? Math.max(...preparedHops.map(h => h.length)) : 0;
|
|
1304
|
+
const finalGasLimit = _calculateGasLimit(config, isNative, hasHops, maxHopCount);
|
|
1305
|
+
const nativeGasLimit = (config.prefer21000ForNative ?? false) ? BRIBE_GAS_LIMIT : finalGasLimit;
|
|
1306
|
+
// 与 disperse/swap.ts 保持一致:多跳场景固定 gas limit
|
|
1307
|
+
const nativeTransferGasLimit = NATIVE_TRANSFER_GAS_LIMIT;
|
|
1308
|
+
const erc20TransferGasLimit = GAS_LIMITS.ERC20_TRANSFER;
|
|
1309
|
+
const [gasPrice, decimals] = await Promise.all([
|
|
1310
|
+
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
1311
|
+
isNative ? Promise.resolve(18) : Promise.resolve(tokenDecimals ?? await _getErc20DecimalsMerkle(provider, tokenAddress, chainIdNum))
|
|
1312
|
+
]);
|
|
1313
|
+
const gasFields = buildGasFields(txType, gasPrice);
|
|
1314
|
+
const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
1315
|
+
// ✅ 费用:按地址数量收取固定费用(原生代币),由第一个 sender 支付
|
|
1316
|
+
const extractProfit = shouldExtractProfit(config);
|
|
1317
|
+
const profitAddr = getProfitRecipient();
|
|
1318
|
+
let totalAmountBeforeProfit = 0n;
|
|
1319
|
+
let totalProfitNative = 0n;
|
|
1320
|
+
const parsedAmountsWei = normalizedAmounts.map(v => {
|
|
1321
|
+
const amt = isNative ? ethers.parseEther(v) : ethers.parseUnits(v, decimals);
|
|
1322
|
+
totalAmountBeforeProfit += amt;
|
|
1323
|
+
return amt;
|
|
1324
|
+
});
|
|
1325
|
+
// ✅ 按地址数量收取固定费用
|
|
1326
|
+
if (extractProfit && pairCount > 0) {
|
|
1327
|
+
totalProfitNative = calculateTransferFee(chainIdNum, pairCount);
|
|
1328
|
+
console.log(`[pairwise] 按地址收费: ${pairCount} 个地址 × ${ethers.formatEther(calculateTransferFee(chainIdNum, 1))} = ${ethers.formatEther(totalProfitNative)} 原生币`);
|
|
1329
|
+
}
|
|
1330
|
+
// ✅ BSC 链贿赂支持(由第一个 sender 支付)
|
|
1331
|
+
const bribeAmount = chainIdNum === CHAINS.BSC.chainId ? getBribeAmount(config) : 0n;
|
|
1332
|
+
const needBribeTx = bribeAmount > 0n;
|
|
1333
|
+
const bribeSignedTx = [];
|
|
1334
|
+
// 计算每个 sender 需要的 nonce 数量(聚合到地址)
|
|
1335
|
+
const senderWallets = senderPrivateKeys.map(pk => new Wallet(pk, provider));
|
|
1336
|
+
const senderAddrLowerList = senderWallets.map(w => w.address.toLowerCase());
|
|
1337
|
+
const firstSenderAddrLower = senderAddrLowerList[0];
|
|
1338
|
+
const nonceNeedBySender = new Map();
|
|
1339
|
+
for (let i = 0; i < pairCount; i++) {
|
|
1340
|
+
const addrLower = senderAddrLowerList[i];
|
|
1341
|
+
const hopLen = preparedHops ? preparedHops[i].length : 0;
|
|
1342
|
+
const perPairTxCount = (() => {
|
|
1343
|
+
if (isNative)
|
|
1344
|
+
return 1;
|
|
1345
|
+
return hopLen > 0 ? 2 : 1;
|
|
1346
|
+
})();
|
|
1347
|
+
const cur = nonceNeedBySender.get(addrLower);
|
|
1348
|
+
if (cur)
|
|
1349
|
+
cur.count += perPairTxCount;
|
|
1350
|
+
else
|
|
1351
|
+
nonceNeedBySender.set(addrLower, { wallet: senderWallets[i], count: perPairTxCount });
|
|
1352
|
+
}
|
|
1353
|
+
// ✅ 贿赂交易需要 1 个额外的 nonce
|
|
1354
|
+
if (needBribeTx) {
|
|
1355
|
+
const cur = nonceNeedBySender.get(firstSenderAddrLower);
|
|
1356
|
+
if (cur)
|
|
1357
|
+
cur.count += 1;
|
|
1358
|
+
}
|
|
1359
|
+
if (extractProfit && totalProfitNative > 0n) {
|
|
1360
|
+
const cur = nonceNeedBySender.get(firstSenderAddrLower);
|
|
1361
|
+
if (cur)
|
|
1362
|
+
cur.count += 1;
|
|
1363
|
+
}
|
|
1364
|
+
const nonceQueueBySender = new Map();
|
|
1365
|
+
for (const [addrLower, entry] of nonceNeedBySender.entries()) {
|
|
1366
|
+
const count = entry.count;
|
|
1367
|
+
const nonces = (addrLower === firstSenderAddrLower && startNonce !== undefined)
|
|
1368
|
+
? Array.from({ length: count }, (_, i) => startNonce + i)
|
|
1369
|
+
: await nonceManager.getNextNonceBatch(entry.wallet, count);
|
|
1370
|
+
nonceQueueBySender.set(addrLower, nonces);
|
|
1371
|
+
}
|
|
1372
|
+
const popNonce = (addrLower) => {
|
|
1373
|
+
const q = nonceQueueBySender.get(addrLower);
|
|
1374
|
+
if (!q || q.length === 0)
|
|
1375
|
+
throw new Error(`nonce queue empty for sender ${addrLower}`);
|
|
1376
|
+
return q.shift();
|
|
1377
|
+
};
|
|
1378
|
+
const txsToSign = [];
|
|
1379
|
+
const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
|
|
1380
|
+
const erc20HopGasFee = erc20TransferGasLimit * gasPrice;
|
|
1381
|
+
const nativeHopGasFeeForErc20 = nativeTransferGasLimit * gasPrice;
|
|
1382
|
+
for (let i = 0; i < pairCount; i++) {
|
|
1383
|
+
const senderWallet = senderWallets[i];
|
|
1384
|
+
const senderLower = senderAddrLowerList[i];
|
|
1385
|
+
const to = receiverAddresses[i];
|
|
1386
|
+
const amtWei = parsedAmountsWei[i];
|
|
1387
|
+
const hopChain = preparedHops ? preparedHops[i] : [];
|
|
1388
|
+
if (!hopChain || hopChain.length === 0) {
|
|
1389
|
+
const nonce = popNonce(senderLower);
|
|
1390
|
+
if (isNative) {
|
|
1391
|
+
txsToSign.push({
|
|
1392
|
+
wallet: senderWallet,
|
|
1393
|
+
tx: { to, value: amtWei, nonce, gasLimit: nativeGasLimit, chainId: chainIdNum, ...gasFields }
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
const data = iface.encodeFunctionData('transfer', [to, amtWei]);
|
|
1398
|
+
txsToSign.push({
|
|
1399
|
+
wallet: senderWallet,
|
|
1400
|
+
tx: { to: tokenAddress, data, value: 0n, nonce, gasLimit: finalGasLimit, chainId: chainIdNum, ...gasFields }
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const fullChain = [senderWallet, ...hopChain.map(w => new Wallet(typeof w === 'string' ? w : w.privateKey, provider))];
|
|
1406
|
+
const addresses = [...fullChain.map(w => w.address), to];
|
|
1407
|
+
if (isNative) {
|
|
1408
|
+
for (let j = 0; j < addresses.length - 1; j++) {
|
|
1409
|
+
const fromWallet = fullChain[j];
|
|
1410
|
+
const toAddress = addresses[j + 1];
|
|
1411
|
+
const nonce = j === 0 ? popNonce(senderLower) : 0;
|
|
1412
|
+
const remainingHops = addresses.length - 2 - j;
|
|
1413
|
+
const additionalGas = nativeHopGasFee * BigInt(remainingHops);
|
|
1414
|
+
const transferValue = amtWei + additionalGas;
|
|
1415
|
+
txsToSign.push({
|
|
1416
|
+
wallet: fromWallet,
|
|
1417
|
+
tx: { to: toAddress, value: transferValue, nonce, gasLimit: nativeTransferGasLimit, chainId: chainIdNum, ...gasFields }
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
const hopGasNeeds = [];
|
|
1423
|
+
for (let j = hopChain.length - 1; j >= 0; j--) {
|
|
1424
|
+
if (j === hopChain.length - 1)
|
|
1425
|
+
hopGasNeeds.unshift(erc20HopGasFee);
|
|
1426
|
+
else {
|
|
1427
|
+
const nextHopGas = hopGasNeeds[0];
|
|
1428
|
+
hopGasNeeds.unshift(nativeHopGasFeeForErc20 + erc20HopGasFee + nextHopGas);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
txsToSign.push({
|
|
1432
|
+
wallet: senderWallet,
|
|
1433
|
+
tx: {
|
|
1434
|
+
to: fullChain[1].address,
|
|
1435
|
+
value: hopGasNeeds[0],
|
|
1436
|
+
nonce: popNonce(senderLower),
|
|
1437
|
+
gasLimit: nativeTransferGasLimit,
|
|
1438
|
+
chainId: chainIdNum,
|
|
1439
|
+
...gasFields
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
const mainToFirstHopData = iface.encodeFunctionData('transfer', [fullChain[1].address, amtWei]);
|
|
1443
|
+
txsToSign.push({
|
|
1444
|
+
wallet: senderWallet,
|
|
1445
|
+
tx: {
|
|
1446
|
+
to: tokenAddress,
|
|
1447
|
+
data: mainToFirstHopData,
|
|
1448
|
+
value: 0n,
|
|
1449
|
+
nonce: popNonce(senderLower),
|
|
1450
|
+
gasLimit: erc20TransferGasLimit,
|
|
1451
|
+
chainId: chainIdNum,
|
|
1452
|
+
...gasFields
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
// hop wallets:逐层传递
|
|
1456
|
+
for (let j = 1; j < fullChain.length; j++) {
|
|
1457
|
+
const fromWallet = fullChain[j];
|
|
1458
|
+
const toAddress = addresses[j + 1];
|
|
1459
|
+
const isLastHop = j === fullChain.length - 1;
|
|
1460
|
+
if (!isLastHop) {
|
|
1461
|
+
const gasToTransfer = hopGasNeeds[j];
|
|
1462
|
+
txsToSign.push({
|
|
1463
|
+
wallet: fromWallet,
|
|
1464
|
+
tx: {
|
|
1465
|
+
to: toAddress,
|
|
1466
|
+
value: gasToTransfer,
|
|
1467
|
+
nonce: 0,
|
|
1468
|
+
gasLimit: nativeTransferGasLimit,
|
|
1469
|
+
chainId: chainIdNum,
|
|
1470
|
+
...gasFields
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, amtWei]);
|
|
1474
|
+
txsToSign.push({
|
|
1475
|
+
wallet: fromWallet,
|
|
1476
|
+
tx: {
|
|
1477
|
+
to: tokenAddress,
|
|
1478
|
+
data: erc20Data,
|
|
1479
|
+
value: 0n,
|
|
1480
|
+
nonce: 1,
|
|
1481
|
+
gasLimit: erc20TransferGasLimit,
|
|
1482
|
+
chainId: chainIdNum,
|
|
1483
|
+
...gasFields
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
else {
|
|
1488
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, amtWei]);
|
|
1489
|
+
txsToSign.push({
|
|
1490
|
+
wallet: fromWallet,
|
|
1491
|
+
tx: {
|
|
1492
|
+
to: tokenAddress,
|
|
1493
|
+
data: erc20Data,
|
|
1494
|
+
value: 0n,
|
|
1495
|
+
nonce: 0,
|
|
1496
|
+
gasLimit: erc20TransferGasLimit,
|
|
1497
|
+
chainId: chainIdNum,
|
|
1498
|
+
...gasFields
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (needBribeTx) {
|
|
1506
|
+
const firstSenderWallet = senderWallets[0];
|
|
1507
|
+
const bribeNonce = popNonce(firstSenderAddrLower);
|
|
1508
|
+
const bribeTx = await firstSenderWallet.signTransaction({
|
|
1509
|
+
to: BLOCKRAZOR_BUILDER_EOA,
|
|
1510
|
+
value: bribeAmount,
|
|
1511
|
+
nonce: bribeNonce,
|
|
1512
|
+
gasLimit: BRIBE_GAS_LIMIT,
|
|
1513
|
+
chainId: chainIdNum,
|
|
1514
|
+
...gasFields
|
|
1515
|
+
});
|
|
1516
|
+
bribeSignedTx.push(bribeTx);
|
|
1517
|
+
console.log(`[pairwise] 贿赂交易已添加: ${ethers.formatEther(bribeAmount)} BNB`);
|
|
1518
|
+
}
|
|
1519
|
+
const signedTxs = await Promise.all(txsToSign.map(({ wallet, tx }) => wallet.signTransaction(tx)));
|
|
1520
|
+
// 利润多跳(固定 2 hop)
|
|
1521
|
+
let profitHopWallets;
|
|
1522
|
+
if (extractProfit && totalProfitNative > 0n) {
|
|
1523
|
+
const firstSenderWallet = senderWallets[0];
|
|
1524
|
+
const profitNonce = popNonce(firstSenderAddrLower);
|
|
1525
|
+
const profitHopResult = await buildProfitHopTransactions({
|
|
1526
|
+
provider,
|
|
1527
|
+
payerWallet: firstSenderWallet,
|
|
1528
|
+
profitAmount: totalProfitNative,
|
|
1529
|
+
profitRecipient: profitAddr,
|
|
1530
|
+
hopCount: PROFIT_HOP_COUNT,
|
|
1531
|
+
gasPrice,
|
|
1532
|
+
chainId: chainIdNum,
|
|
1533
|
+
txType,
|
|
1534
|
+
startNonce: profitNonce
|
|
1535
|
+
});
|
|
1536
|
+
signedTxs.push(...profitHopResult.signedTransactions);
|
|
1537
|
+
profitHopWallets = profitHopResult.hopWallets;
|
|
1538
|
+
}
|
|
1539
|
+
return {
|
|
1540
|
+
signedTransactions: [...bribeSignedTx, ...signedTxs], // ✅ 贿赂交易放在最前面
|
|
1541
|
+
hopWallets: preparedHops || undefined,
|
|
1542
|
+
profitHopWallets,
|
|
1543
|
+
metadata: extractProfit ? {
|
|
1544
|
+
pairCount,
|
|
1545
|
+
isNative,
|
|
1546
|
+
tokenAddress: isNative ? undefined : tokenAddress,
|
|
1547
|
+
totalAmount: isNative ? ethers.formatEther(totalAmountBeforeProfit) : ethers.formatUnits(totalAmountBeforeProfit, decimals),
|
|
1548
|
+
profitAmount: ethers.formatEther(totalProfitNative),
|
|
1549
|
+
profitRecipient: profitAddr,
|
|
1550
|
+
hasBribeTx: bribeSignedTx.length > 0, // ✅ 标记是否有贿赂交易
|
|
1551
|
+
bribeAmount: bribeSignedTx.length > 0 ? ethers.formatEther(bribeAmount) : undefined
|
|
1552
|
+
} : undefined
|
|
1553
|
+
};
|
|
1554
|
+
}
|