diff --git a/actions/transferAll/core.ts b/actions/transferAll/core.ts index e071110..1400f95 100644 --- a/actions/transferAll/core.ts +++ b/actions/transferAll/core.ts @@ -1,17 +1,12 @@ import { BigNumber, utils } from "ethers"; -import { - Account as SNAccount, - ec, - number, - SequencerProvider, - uint256, -} from "starknet"; +import { number, uint256 } from "starknet"; import { compileCalldata } from "starknet/dist/utils/stark"; import { Account } from "../../ui/pickAccounts"; import TOKENS from "../../default-tokens.json"; import { Ora } from "ora"; import { oraLog } from "../../oraLog"; import { estimateFee, execute } from "../../execute"; +import { formatAddress } from "../../addressFormatting"; export async function transferAll(acc: Account, newAddress: string, ora: Ora) { const { privateKey } = acc; @@ -19,7 +14,6 @@ export async function transferAll(acc: Account, newAddress: string, ora: Ora) { throw new Error("No private key for account to credit"); } - const provider = new SequencerProvider({ network: acc.networkId as any }); const tokens = TOKENS.filter((t) => t.network === acc.networkId); const calls = Object.entries(acc.balances) .filter(([, balance]) => utils.parseEther(balance).gt(0)) @@ -47,36 +41,44 @@ export async function transferAll(acc: Account, newAddress: string, ora: Ora) { if (calls.length) { const { suggestedMaxFee } = await estimateFee(acc, calls); - const callsWithFee = calls.map((c) => { - const tokenDetails = tokens.find((t) => t.symbol === "ETH"); - if (c.contractAddress === tokenDetails?.address) { - const balance = acc.balances[c.contractAddress]; - return { - ...c, - calldata: compileCalldata({ - to: newAddress.toLowerCase(), - value: { - type: "struct", - ...uint256.bnToUint256( - number.toBN( - utils - .parseUnits(balance, tokenDetails?.decimals ?? 18) - .sub(number.toHex(suggestedMaxFee)) - .toString() - ) - ), - }, - }), - }; - } - return c; - }); + const callsWithFee = calls + .map((c) => { + const tokenDetails = tokens.find((t) => t.symbol === "ETH"); + if (c.contractAddress === tokenDetails?.address) { + const balance = acc.balances[c.contractAddress]; + const amount = utils + .parseUnits(balance, tokenDetails?.decimals ?? 18) + .sub(number.toHex(suggestedMaxFee)); + + if (amount.lte(0)) { + ora.info( + `Account ${formatAddress( + acc.address + )} has not enough ETH to do a transfer` + ); + return false; + } + + return { + ...c, + calldata: compileCalldata({ + to: newAddress.toLowerCase(), + value: { + type: "struct", + ...uint256.bnToUint256(number.toBN(amount.toString())), + }, + }), + }; + } + return c; + }) + .filter(Boolean); // execute with suggested max fee substracted from a potential eth transfer const transaction = await execute(acc, callsWithFee); oraLog(ora, `Transaction ${transaction.transaction_hash} created`); - await provider.waitForTransaction(transaction.transaction_hash); + return transaction.transaction_hash; } } diff --git a/actions/transferAll/ui.ts b/actions/transferAll/ui.ts index 5db6259..559defd 100644 --- a/actions/transferAll/ui.ts +++ b/actions/transferAll/ui.ts @@ -4,6 +4,30 @@ import { ValidationError } from "yup"; import { addressSchema } from "../../schema"; import { Account } from "../../ui/pickAccounts"; import { transferAll } from "./core"; +import { SequencerProvider } from "starknet-4220"; + +type PromiseFactory = () => Promise; + +type PromiseResult = + | { status: "fulfilled"; value: T } + | { status: "rejected"; reason: any }; + +async function allSettled( + promiseFactories: PromiseFactory[] +): Promise[]> { + const results: PromiseResult[] = []; + + for (const promiseFactory of promiseFactories) { + try { + const value = await promiseFactory(); + results.push({ status: "fulfilled", value }); + } catch (reason) { + results.push({ status: "rejected", reason }); + } + } + + return results; +} export async function showTransferAll(accounts: Account[]) { const { toAddress } = await prompts( @@ -30,16 +54,32 @@ export async function showTransferAll(accounts: Account[]) { const spinner = ora("Transfering tokens").start(); - const transferResults = await Promise.allSettled( - accounts.map(async (acc) => transferAll(acc, toAddress, spinner)) + const transferResults = await allSettled( + accounts.map((acc) => () => transferAll(acc, toAddress, spinner)) ); - transferResults.forEach((result) => { - if (result.status === "rejected") { - spinner.fail(result.reason?.toString()); - } + const transactions = transferResults + .map((result) => { + if (result.status === "rejected") { + spinner.fail(result.reason?.toString()); + return undefined; + } + return result.value; + }) + .filter((tx) => !!tx); + + const provider = new SequencerProvider({ + network: accounts[0].networkId as any, }); + spinner.info(`Waiting for ${transactions.length} transactions`); + await Promise.all( + transactions.map((tx) => { + if (!tx) return; + return provider.waitForTransaction(tx); + }) + ); + if (transferResults.every((result) => result.status === "fulfilled")) { spinner.succeed("Transfers complete"); } else if (transferResults.some((result) => result.status === "fulfilled")) { diff --git a/execute.ts b/execute.ts index 7ac4831..8abe98c 100644 --- a/execute.ts +++ b/execute.ts @@ -1,7 +1,7 @@ import { SequencerProvider as NewProvider, Account as NewAccount, -} from "starknet-490"; +} from "starknet-4220"; import { Call, SequencerProvider as OldProvider, @@ -10,6 +10,7 @@ import { } from "starknet"; import { BigNumber } from "ethers"; import { Account } from "./ui/pickAccounts"; +import { lte } from "semver"; export async function estimateFee(account: Account, call: Call[] | Call) { const calls = Array.isArray(call) ? call : [call]; @@ -42,15 +43,35 @@ export async function execute(account: Account, call: Call[] | Call) { "Account cant be controlled with the selected private key or seed" ); } - try { - const oldProvider = new OldProvider({ network: account.networkId as any }); - const a = new OldAccount(oldProvider, lowerCaseAddress, keyPair); - return await a.execute(calls); - } catch (e) { - const newProvider = new NewProvider({ network: account.networkId as any }); - const a = new NewAccount(newProvider, lowerCaseAddress, keyPair); - return a.execute(calls).catch((e) => { - throw e; - }); + if (account.version && lte(account.version, "0.2.2")) { + try { + const oldProvider = new OldProvider({ + network: account.networkId as any, + }); + const a = new OldAccount(oldProvider, lowerCaseAddress, keyPair); + return await a.execute(calls); + } catch (e) { + console.warn("Fallback to new provider", (e as any)?.errorCode); + const newProvider = new NewProvider({ + network: account.networkId as any, + }); + const a = new NewAccount(newProvider, lowerCaseAddress, keyPair); + return await a.execute(calls); + } + } else { + try { + const newProvider = new NewProvider({ + network: account.networkId as any, + }); + const a = new NewAccount(newProvider, lowerCaseAddress, keyPair); + return await a.execute(calls); + } catch (e) { + console.warn("Fallback to old provider", (e as any)?.errorCode); + const oldProvider = new OldProvider({ + network: account.networkId as any, + }); + const a = new OldAccount(oldProvider, lowerCaseAddress, keyPair); + return await a.execute(calls); + } } } diff --git a/fetchPolyfill.ts b/fetchPolyfill.ts new file mode 100644 index 0000000..ceb9495 --- /dev/null +++ b/fetchPolyfill.ts @@ -0,0 +1,23 @@ +import ora from "ora"; + +type Fetch = typeof fetch; + +const originalFetch: Fetch = fetch.bind(globalThis); + +const wait = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +const customFetch: Fetch = async (input, init): Promise => { + const response = await originalFetch(input, init); + + if (response.status === 429) { + // Wait for one minute before retrying + ora().warn("429 - waiting 1 minute before retrying request"); + await wait(60 * 1000); + return originalFetch(input, init); + } + + return response; +}; + +globalThis.fetch = customFetch; diff --git a/index.ts b/index.ts index 2967a2f..f588ca2 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,7 @@ #!/usr/bin/env npx ts-node +import "@total-typescript/ts-reset"; +import "./fetchPolyfill"; + import { program } from "commander"; import ora from "ora"; import { Account, pickAccounts } from "./ui/pickAccounts"; diff --git a/issues/deploy/fix.ts b/issues/deploy/fix.ts index 5eee02b..936def1 100644 --- a/issues/deploy/fix.ts +++ b/issues/deploy/fix.ts @@ -5,7 +5,7 @@ import { SequencerProvider, stark, hash, -} from "starknet-490"; +} from "starknet-4220"; import { PROXY_CONTRACT_CLASS_HASHES } from "../../getAccounts"; import { getVersion } from "../../getVersion"; import { oraLog } from "../../oraLog"; diff --git a/package.json b/package.json index bd5854a..b18f52b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "outputPath": "bin" }, "devDependencies": { + "@total-typescript/ts-reset": "^0.4.2", "@types/lodash.chunk": "^4.2.7", "@types/prompts": "^2.0.14", "@types/semver": "^7.3.10", @@ -40,9 +41,9 @@ "ora": "5", "prompts": "^2.4.2", "semver": "^7.3.7", - "starknet-490": "npm:starknet@4.9.0", "starknet": "4.1.0", "starknet-390": "npm:starknet@3.9.0", + "starknet-4220": "npm:starknet@4.22.0", "yup": "^1.0.0-beta.4" } } diff --git a/yarn.lock b/yarn.lock index 723a424..b67a5d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -577,6 +577,11 @@ "@noble/hashes" "~1.1.1" "@scure/base" "~1.1.0" +"@total-typescript/ts-reset@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@total-typescript/ts-reset/-/ts-reset-0.4.2.tgz#c564c173ba09973968e1046c93965b7a257878a4" + integrity sha512-vqd7ZUDSrXFVT1n8b2kc3LnklncDQFPvR58yUS1kEP23/nHPAO9l1lMjUfnPrXYYk4Hj54rrLKMW5ipwk7k09A== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -1905,10 +1910,10 @@ slash@^3.0.0: superstruct "^0.15.3" url-join "^4.0.1" -"starknet-490@npm:starknet@4.9.0", starknet@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/starknet/-/starknet-4.9.0.tgz#1d0b5a1ff532a41b84a619f3d7c3f833f67dad2d" - integrity sha512-0Y9Nce0msMs1k3jid93ZuPD2e3yW42FUuMFChRTaq1qcjP8G/fL+5+/zuq441VQ82/JxAHnVqBMvpmMgn5yJSA== +"starknet-4220@npm:starknet@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/starknet/-/starknet-4.22.0.tgz#8d0c628e2a8e868ee9b4757afe89f07b05ec55ff" + integrity sha512-jC9Taxb6a/ht9zmS1LU/DSLfwJKpgCJnE9AktVksc5SE/+jQMpqxsq6fm7PRiqupjiqRC1DOS8N47cj+KaGv4Q== dependencies: "@ethersproject/bytes" "^5.6.1" bn.js "^5.2.1" @@ -1919,7 +1924,7 @@ slash@^3.0.0: json-bigint "^1.0.0" minimalistic-assert "^1.0.1" pako "^2.0.4" - ts-custom-error "^3.2.0" + ts-custom-error "^3.3.1" url-join "^4.0.1" starknet@4.1.0: @@ -1939,6 +1944,23 @@ starknet@4.1.0: ts-custom-error "^3.2.0" url-join "^4.0.1" +starknet@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/starknet/-/starknet-4.9.0.tgz#1d0b5a1ff532a41b84a619f3d7c3f833f67dad2d" + integrity sha512-0Y9Nce0msMs1k3jid93ZuPD2e3yW42FUuMFChRTaq1qcjP8G/fL+5+/zuq441VQ82/JxAHnVqBMvpmMgn5yJSA== + dependencies: + "@ethersproject/bytes" "^5.6.1" + bn.js "^5.2.1" + elliptic "^6.5.4" + ethereum-cryptography "^1.0.3" + hash.js "^1.1.7" + isomorphic-fetch "^3.0.0" + json-bigint "^1.0.0" + minimalistic-assert "^1.0.1" + pako "^2.0.4" + ts-custom-error "^3.2.0" + url-join "^4.0.1" + stream-meter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" @@ -2069,7 +2091,7 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -ts-custom-error@^3.2.0: +ts-custom-error@^3.2.0, ts-custom-error@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1" integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==