4 |
5 |
6 |
7 |
8 |
9 | Sweeposaurus is a tool that helps you move all your ETH and ERC-20 tokens from one wallet to another. For
10 | example, if you want to move all your funds from MetaMask to Ledger, this tool would simplify that process.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | First your wallet is scanned to check token balances of over
19 |
24 | 1600 tokens . A table will be shown displaying your balance of each token, with your ETH balance in the last row. You
26 | can then enter how much of each token to send, along with the address to send funds to. Sweeposaurus will
27 | queue up each transfer, and all you need to do is confirm each one.
28 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
It does not.
40 |
41 | Unless you use a smart contract wallet, there is no way to reduce the number of transactions. This app
42 | simply makes the process less tedious.
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | If some tokens are missing, it means we don't scan for them. Create an
53 | issue with the
54 | missing token name(s). If you have the token addresses also, that is preferred.
55 |
56 |
57 | If you are not a GitHub user, feel free to message me on
58 | Twitter with the above
59 | info.
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Maybe! Create an
70 | issue with the
71 | feature details.
72 |
73 |
74 | If you are not a GitHub user, feel free to message me on
75 | Twitter with the feature
76 | request.
77 |
78 |
79 |
80 |
81 |
82 |
87 |
88 |
89 | You can use the
90 | Cancel Ethereum Transactions
93 | application to cancel or speed up your tansfers.
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | The GitHub repository is
102 | here. The code for
103 | transaction generation can be found in
104 |
109 | this file .
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
125 |
--------------------------------------------------------------------------------
/src/store/wallet.ts:
--------------------------------------------------------------------------------
1 | import { computed, ref } from '@vue/composition-api';
2 | import { ethers } from 'ethers';
3 | import { StaticJsonRpcProvider } from '@ethersproject/providers';
4 | import { TokenList } from '@uniswap/token-lists';
5 | import { MulticallResponse, Signer, Provider, TokenDetails } from 'components/models';
6 | import multicallInfo from 'src/contracts/multicall.json';
7 | import erc20 from 'src/contracts/erc20.json';
8 | import useAnalytics from 'src/utils/analytics';
9 |
10 | const { Zero } = ethers.constants;
11 |
12 | export const MAINNET_RPC_URL = `https://mainnet.infura.io/v3/${String(process.env.INFURA_ID)}`;
13 | export const MAINNET_PROVIDER = new StaticJsonRpcProvider(MAINNET_RPC_URL);
14 |
15 | // Returns an address with the following format: 0x1234...abcd
16 | const formatAddress = (address: string) => `${address.slice(0, 6)}...${address.slice(38)}`;
17 |
18 | /**
19 | * State is handled in reusable components, where each component is its own self-contained
20 | * file consisting of one function defined used the composition API.
21 | *
22 | * Since we want the wallet state to be shared between all instances when this file is imported,
23 | * we defined state outside of the function definition.
24 | */
25 |
26 | // ============================================= State =============================================
27 | // We do not publicly expose the state to provide control over when and how it's changed. It
28 | // can only be changed through actions and mutations, and it can only be accessed with getters.
29 | // As a result, only actions, mutations, and getters are returned from this function.
30 | const provider = ref(undefined);
31 | const signer = ref(undefined);
32 | const chainId = ref(undefined);
33 | const userAddress = ref(undefined);
34 | const userDisplayName = ref(undefined);
35 | const avatar = ref('');
36 | const balances = ref([]);
37 |
38 | export default function useWalletStore() {
39 | // =========================================== Actions ===========================================
40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
41 | async function setProvider(p: any) {
42 | provider.value = new ethers.providers.Web3Provider(p);
43 | signer.value = provider.value.getSigner();
44 | const [_userAddress, _network] = await Promise.all([signer.value.getAddress(), provider.value.getNetwork()]);
45 | userAddress.value = _userAddress;
46 | chainId.value = _network.chainId;
47 | userDisplayName.value = formatAddress(_userAddress);
48 |
49 | const userEns = await MAINNET_PROVIDER.lookupAddress(_userAddress);
50 | if (typeof userEns === 'string') {
51 | // ENS address must exist
52 | avatar.value = await MAINNET_PROVIDER.getAvatar(userEns);
53 | }
54 | userDisplayName.value = userEns || formatAddress(_userAddress);
55 | }
56 |
57 | async function fetchTokenList() {
58 | const jsonFetch = (url: string) => fetch(url).then((res) => res.json());
59 | const url = 'https://tokens.uniswap.org/'; // Includes tokens for various networks
60 | const response = (await jsonFetch(url)) as TokenList;
61 | return response.tokens.filter((token) => token.chainId === chainId.value);
62 | }
63 |
64 | async function scan() {
65 | const { logEvent } = useAnalytics();
66 | logEvent('scan-started');
67 |
68 | // Get token list
69 | const tokenList = await fetchTokenList();
70 |
71 | // Get Multicall instance
72 | const multicall = new ethers.Contract(multicallInfo.address, multicallInfo.abi, signer.value);
73 |
74 | // Generate balance calls using Multicall contract
75 | const userAddr = userAddress.value || (await signer.value?.getAddress());
76 | const calls = tokenList.map((token) => {
77 | const { address: tokenAddress } = token;
78 | const tokenContract = new ethers.Contract(tokenAddress, erc20.abi, signer.value);
79 | return {
80 | target: tokenAddress,
81 | callData: tokenContract.interface.encodeFunctionData('balanceOf', [userAddr]),
82 | };
83 | });
84 |
85 | // Generate array of promises to get token balances + ETH balance
86 | const ethBalancePromise = signer.value?.getBalance();
87 | const promises = [multicall.callStatic.aggregate(calls), ethBalancePromise];
88 |
89 | // Wait for promises to resolve
90 | const responses = await Promise.all(promises);
91 | const multicallResponse = responses[0] as MulticallResponse;
92 | const ethResponse = responses[1] as ethers.BigNumber;
93 | const tokenBalances = multicallResponse.returnData; // token balances from multicall
94 |
95 | // Create array of all tokens with their balance and only keep nonzero ones
96 | balances.value = tokenList
97 | .map((token, index) => {
98 | // `tokenBalances[index] === '0x'` occurs when a token in the token list is an EOA or non ERC-20 contract, so
99 | // instead of returning a balance we get no data
100 | const balance = tokenBalances[index] === '0x' ? Zero : ethers.BigNumber.from(tokenBalances[index]);
101 | return { ...token, balance, amountToSend: 'max' };
102 | })
103 | .filter((token) => token.balance.gt(Zero))
104 | .sort((token1, token2) => token1.symbol.localeCompare(token2.symbol));
105 |
106 | // Append ETH to the list
107 | const ethToken = {
108 | chainId: 1,
109 | address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
110 | name: 'Ether',
111 | decimals: 18,
112 | symbol: 'ETH',
113 | logoURI: 'logos/eth.png',
114 | };
115 | balances.value.push({
116 | ...ethToken,
117 | balance: ethResponse,
118 | amountToSend: 'max',
119 | });
120 | }
121 |
122 | return {
123 | provider: computed(() => provider.value),
124 | signer: computed(() => signer.value),
125 | avatar: computed(() => avatar.value),
126 | chainId: computed(() => chainId.value),
127 | userAddress: computed(() => userAddress.value),
128 | userDisplayName: computed(() => userDisplayName.value),
129 | setProvider,
130 | scan,
131 | balances: computed(() => balances.value),
132 | };
133 | }
134 |
--------------------------------------------------------------------------------
/quasar.conf.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only
3 | * the ES6 features that are supported by your Node version. https://node.green/
4 | */
5 |
6 | // Configuration for your app
7 | // https://quasar.dev/quasar-cli/quasar-conf-js
8 | /* eslint-env node */
9 | /* eslint-disable @typescript-eslint/no-var-requires */
10 | const { configure } = require('quasar/wrappers');
11 |
12 | module.exports = configure(function (ctx) {
13 | return {
14 | // https://quasar.dev/quasar-cli/supporting-ts
15 | supportTS: {
16 | tsCheckerConfig: {
17 | eslint: true,
18 | },
19 | },
20 |
21 | // https://quasar.dev/quasar-cli/prefetch-feature
22 | // preFetch: true,
23 |
24 | // app boot file (/src/boot)
25 | // --> boot files are part of "main.js"
26 | // https://quasar.dev/quasar-cli/boot-files
27 | boot: ['composition-api', 'error-handler'],
28 |
29 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
30 | css: ['app.sass'],
31 |
32 | // https://github.com/quasarframework/quasar/tree/dev/extras
33 | extras: [
34 | // 'ionicons-v4',
35 | // 'mdi-v5',
36 | 'fontawesome-v5',
37 | // 'eva-icons',
38 | // 'themify',
39 | // 'line-awesome',
40 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
41 |
42 | // 'roboto-font', // optional, you are not bound to it
43 | // 'material-icons', // optional, you are not bound to it
44 | ],
45 |
46 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
47 | build: {
48 | vueRouterMode: 'history', // available values: 'hash', 'history'
49 |
50 | // transpile: false,
51 |
52 | // Add dependencies for transpiling with Babel (Array of string/regex)
53 | // (from node_modules, which are by default not transpiled).
54 | // Applies only if "transpile" is set to true.
55 | // transpileDependencies: [],
56 |
57 | // rtl: false, // https://quasar.dev/options/rtl-support
58 | // preloadChunks: true,
59 | // showProgress: false,
60 | // gzip: true,
61 | // analyze: true,
62 |
63 | // Options below are automatically set depending on the env, set them if you want to override
64 | // extractCSS: false,
65 |
66 | // https://quasar.dev/quasar-cli/handling-webpack
67 | extendWebpack(cfg) {
68 | // linting is slow in TS projects, we execute it only for production builds
69 | if (ctx.prod) {
70 | cfg.module.rules.push({
71 | enforce: 'pre',
72 | test: /\.(js|vue)$/,
73 | loader: 'eslint-loader',
74 | exclude: /node_modules/,
75 | });
76 | }
77 | },
78 | },
79 |
80 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
81 | devServer: {
82 | https: false,
83 | port: 8080,
84 | open: true, // opens browser window automatically
85 | },
86 |
87 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
88 | framework: {
89 | iconSet: 'fontawesome-v5', // Quasar icon set
90 | lang: 'en-us', // Quasar language pack
91 | config: {},
92 |
93 | // Possible values for "importStrategy":
94 | // * 'auto' - (DEFAULT) Auto-import needed Quasar components & directives
95 | // * 'all' - Manually specify what to import
96 | importStrategy: 'auto',
97 |
98 | // Quasar plugins
99 | plugins: ['LocalStorage', 'Notify'],
100 | },
101 |
102 | // animations: 'all', // --- includes all animations
103 | // https://quasar.dev/options/animations
104 | animations: [],
105 |
106 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
107 | ssr: {
108 | pwa: false,
109 | },
110 |
111 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
112 | pwa: {
113 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
114 | workboxOptions: {}, // only for GenerateSW
115 | manifest: {
116 | name: 'Sweeposaurus',
117 | short_name: 'Sweeposaurus',
118 | description: 'Sweep all of your tokens to another address',
119 | display: 'standalone',
120 | orientation: 'portrait',
121 | background_color: '#ffffff',
122 | theme_color: '#027be3',
123 | icons: [
124 | {
125 | src: 'icons/icon-128x128.png',
126 | sizes: '128x128',
127 | type: 'image/png',
128 | },
129 | {
130 | src: 'icons/icon-192x192.png',
131 | sizes: '192x192',
132 | type: 'image/png',
133 | },
134 | {
135 | src: 'icons/icon-256x256.png',
136 | sizes: '256x256',
137 | type: 'image/png',
138 | },
139 | {
140 | src: 'icons/icon-384x384.png',
141 | sizes: '384x384',
142 | type: 'image/png',
143 | },
144 | {
145 | src: 'icons/icon-512x512.png',
146 | sizes: '512x512',
147 | type: 'image/png',
148 | },
149 | ],
150 | },
151 | },
152 |
153 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
154 | cordova: {
155 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
156 | },
157 |
158 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
159 | capacitor: {
160 | hideSplashscreen: true,
161 | },
162 |
163 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
164 | electron: {
165 | bundler: 'packager', // 'packager' or 'builder'
166 |
167 | packager: {
168 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
169 | // OS X / Mac App Store
170 | // appBundleId: '',
171 | // appCategoryType: '',
172 | // osxSign: '',
173 | // protocol: 'myapp://path',
174 | // Windows only
175 | // win32metadata: { ... }
176 | },
177 |
178 | builder: {
179 | // https://www.electron.build/configuration/configuration
180 |
181 | appId: 'sweeposaurus',
182 | },
183 |
184 | // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
185 | nodeIntegration: true,
186 |
187 | extendWebpack(/* cfg */) {
188 | // do something with Electron main process Webpack cfg
189 | // chainWebpack also available besides this extendWebpack
190 | },
191 | },
192 | };
193 | });
194 |
--------------------------------------------------------------------------------
/src/pages/Sweep.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Sweep Tokens
8 |
9 |
10 |
11 | This is beta software and has not been heavily tested. Use at your own risk.
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Scanning for tokens in your wallet. This may take a minute...
19 |
20 |
21 |
22 |
23 |
24 |
Step 1: Enter recipient address
25 |
Optionally, you can override the default gas price.
26 |
27 |
28 |
29 |
30 |
31 |
Step 2: Choose tokens to send
32 |
33 | To send the max amount, leave the value untouched. To send nothing, set the value to 0.
34 |
35 |
36 | If you have a lot of tokens, this will be a long table—just keep scrolling for step 3!
37 |