diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ec86a3c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "prettier.tabWidth": 4, + "vetur.format.options.tabSize": 4 +} diff --git a/README.md b/README.md index ebdbc23..42bb16a 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,8 @@ git3://[sender_wallet]@[hub_contract_address or NS]:[chain_id]/ - Required, your repo name ## Example: -- `git3://helloworld` -select `default` wallet, `git3 official hub contract` address, on ETHStorage chainId: 3334, repo name is `helloworld` -It's equl to `git3://default@git3.w3q:3334/helloworld` +- `git3://helloworld@git3.fvm/helloworld` +select `default` wallet, `git3 official hub contract` address, on FVM chainId: 3141, repo name is `helloworld` - `git3://myname.eth@git3hub.eth/helloworld` select `myname.eth` wallet, `git3hub.eth` hub contract address, on ETH Mainnet chainId: 1, repo name is `helloworld` diff --git a/src/common/git3-protocol.ts b/src/common/git3-protocol.ts new file mode 100644 index 0000000..692a338 --- /dev/null +++ b/src/common/git3-protocol.ts @@ -0,0 +1,98 @@ +import { ethers } from "ethers" +import nameServices from "../config/name-services.js" +import { ETHStorage } from "../storage/ETHStorage.js" +import { SLIStorage } from "../storage/SLIStorage.js" +import { getWallet, randomRPC, setupContract } from "./wallet.js" +import network from "../config/evm-network.js" +import abis from "../config/abis.js" + +export type Git3Protocol = { + sender: string + senderAddress: string + hubAddress: string + repoName: string + chainId: number + netConfig: Record + wallet: ethers.Wallet + contract: ethers.Contract + storageClass: any + ns?: Record + nsName?: string + nsDomain?: string +} + +type Option = { + skipRepoName: boolean +} + +export function parseGit3URI( + uri: string, + option: Option = { skipRepoName: false } +): Git3Protocol { + const url = new URL(uri) + let sender = url.username || "default" + let chainId = url.port ? parseInt(url.port) : null + let hub = url.hostname + let hubAddress + let nsName, nsDomain, ns + if (!hub) throw new Error("invalid git3 uri, no hub address") + let repoName = url.pathname.slice(1) + if (!option.skipRepoName && !repoName) + throw new Error("invalid git3 uri, no repo name") + + if (hub.indexOf(".") < 0) { + if (url.hostname.startsWith("0x")) { + hubAddress = url.hostname + } else { + throw new Error("invalid git3 uri, hub must be NS or address") + } + } else { + ;[nsName, nsDomain] = url.hostname.split(".") + ns = nameServices[nsDomain] + if (!ns) throw new Error("invalid name service") + chainId = chainId || ns.chainId + // Todo: resolve name service + // hubAddress = ns.resolver() + } + + if (!chainId) throw new Error("invalid git3 uri, no chainId") + + let netConfig = network[chainId] + if (!netConfig) throw new Error("invalid chainId") + + if (!hubAddress) hubAddress = netConfig.contracts.git3 + + let wallet = getWallet(sender) + + let senderAddress = wallet.address + + // route to different storage + let storageClass, abi + if (chainId == 3334) { + storageClass = ETHStorage + abi = abis.ETHStorage + } else { + storageClass = SLIStorage + abi = abis.SLIStorage + } + let rpc = randomRPC(netConfig.rpc) + const provider = new ethers.providers.JsonRpcProvider(rpc) + + let contract = setupContract(provider, hubAddress, abi, wallet) + wallet = wallet.connect(contract.provider) + + return { + sender, + senderAddress, + hubAddress, + repoName, + chainId, + netConfig, + wallet, + contract, + storageClass, + ns, + nsName, + nsDomain, + } +} diff --git a/src/wallet/tx-manager.ts b/src/common/tx-manager.ts similarity index 77% rename from src/wallet/tx-manager.ts rename to src/common/tx-manager.ts index 29afd40..f12e5af 100644 --- a/src/wallet/tx-manager.ts +++ b/src/common/tx-manager.ts @@ -11,16 +11,18 @@ export class TxManager { gasLimitRatio: number minNonce: number = -1 queueCurrNonce: number = -1 - highestNonce: number = -1 rbfTimes: number boardcastTimes: number waitDistance: number minRBFRatio: number + _initialPromise: Promise | null = null + _deltaCount: number = 0 + constructor( contract: ethers.Contract, chainId: number, - constOptions: { + constOptions?: { blockTimeSec?: number gasLimitRatio?: number rbfTimes?: number @@ -33,12 +35,12 @@ export class TxManager { this.contract = contract this.price = null this.cancel = false - this.blockTimeSec = constOptions.blockTimeSec || 3 - this.gasLimitRatio = constOptions.gasLimitRatio || 1.2 - this.rbfTimes = constOptions.rbfTimes || 3 - this.boardcastTimes = constOptions.boardcastTimes || 3 - this.waitDistance = constOptions.waitDistance || 10 - this.minRBFRatio = constOptions.minRBFRatio || 1.3 + this.blockTimeSec = constOptions?.blockTimeSec || 3 + this.gasLimitRatio = constOptions?.gasLimitRatio || 1.2 + this.rbfTimes = constOptions?.rbfTimes || 3 + this.boardcastTimes = constOptions?.boardcastTimes || 3 + this.waitDistance = constOptions?.waitDistance || 10 + this.minRBFRatio = constOptions?.minRBFRatio || 1.3 } async FreshBaseGas(): Promise { @@ -51,18 +53,50 @@ export class TxManager { // TODO: cancel all tx sended } + async clearPendingNonce(num: number = 1, rbfRatio: number = 1.5) { + const signer = this.contract.signer + let nonce = await this.getNonce() + this._deltaCount++ + console.log("clearPendingNonce", nonce, num) + let price = await this.FreshBaseGas() + let txs = [] + for (let i = 0; i < num; i++) { + let res = signer.sendTransaction({ + to: await signer.getAddress(), + nonce: nonce + i, + gasLimit: 21000, + type: 2, + chainId: this.chainId, + maxFeePerGas: price! + .maxFeePerGas!.mul((rbfRatio * 100) | 0) + .div(100), + maxPriorityFeePerGas: price! + .maxPriorityFeePerGas!.mul((rbfRatio * 100) | 0) + .div(100), + }) + txs.push(res) + } + await Promise.all(txs) + } + + async getNonce(): Promise { + if (!this._initialPromise) { + this._initialPromise = + this.contract.signer.getTransactionCount("pending") + } + const deltaCount = this._deltaCount + this._deltaCount++ + return this._initialPromise.then((initial) => initial + deltaCount) + } + async SendCall(_method: string, _args: any[]): Promise { + const nonce = await this.getNonce() + let unsignedTx = await this.contract.populateTransaction[_method]( ..._args ) - unsignedTx.chainId = this.chainId - if (this.highestNonce < 0) { - this.highestNonce = await this.contract.signer.getTransactionCount() - } - const nonce = this.highestNonce unsignedTx.nonce = nonce - this.highestNonce += 1 - + unsignedTx.chainId = this.chainId // estimateGas check let gasLimit = await this.contract.provider.estimateGas(unsignedTx) unsignedTx.gasLimit = gasLimit @@ -149,6 +183,8 @@ export class TxManager { } catch (e: Error | any) { if (e.code == ethers.errors.NONCE_EXPIRED) { // ignore if tx already in mempool + } else if (e.code == ethers.errors.SERVER_ERROR) { + // ignore if tx already in mempool } else { console.error( "[tx-manager] sendTransaction", diff --git a/src/common/wallet.ts b/src/common/wallet.ts new file mode 100644 index 0000000..076af9a --- /dev/null +++ b/src/common/wallet.ts @@ -0,0 +1,38 @@ +import { mkdirSync, readFileSync } from "fs" +import { ethers } from "ethers" + +export function getWallet(wallet: string | null = "default"): ethers.Wallet { + if (!wallet) wallet = "default" + + // Todo: 0xaddress find wallet + const keyPath = process.env.HOME + "/.git3/keys" + mkdirSync(keyPath, { recursive: true }) + + const content = readFileSync(`${keyPath}/${wallet}`).toString() + const [walletType, key] = content.split("\n") + + let etherWallet = + walletType === "privateKey" + ? new ethers.Wallet(key) + : ethers.Wallet.fromMnemonic(key) + + return etherWallet +} + +export function setupContract( + provider: ethers.providers.JsonRpcProvider, + hubAddress: string, + abi: string, + wallet: ethers.Wallet +): ethers.Contract { + + let contract = new ethers.Contract(hubAddress, abi, provider) + wallet = wallet.connect(provider) + contract = contract.connect(wallet) + + return contract +} + +export function randomRPC(rpcs: string[]): string { + return rpcs[Math.floor(Math.random() * rpcs.length)] +} diff --git a/src/config/evm-network.ts b/src/config/evm-network.ts index a7aaff0..58b7b61 100644 --- a/src/config/evm-network.ts +++ b/src/config/evm-network.ts @@ -2,7 +2,7 @@ const evmNetworks: Record = { 1: { - name: "ethereum", + name: "Ethereum", nativeCurrency: { name: "Ether", symbol: "ETH", diff --git a/src/git-remote-git3/index.ts b/src/git-remote-git3/index.ts index d32d9de..b38bdf2 100644 --- a/src/git-remote-git3/index.ts +++ b/src/git-remote-git3/index.ts @@ -1,10 +1,7 @@ import GitRemoteHelper from "./git-remote-helper.js" import { ApiBaseParams } from "./git-remote-helper.js" import Git from "./git.js" -import { ETHStorage } from "../storage/ETHStorage.js" - -import nameServices from "../config/name-services.js" -import { SLIStorage } from "../storage/SLIStorage.js" +import { parseGit3URI } from "../common/git3-protocol.js" // https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c let git: Git GitRemoteHelper({ @@ -13,46 +10,8 @@ GitRemoteHelper({ stdout: process.stdout, api: { init: async (p: ApiBaseParams) => { - const url = new URL(p.remoteUrl) - let repoName - let git3Address - let chainId = url.port ? parseInt(url.port) : null - if (url.hostname.indexOf(".") < 0) { - if (url.hostname.startsWith("0x")) { - git3Address = url.hostname - repoName = url.pathname.slice(1) - } else { - // use Default git3Address - git3Address = null - repoName = url.hostname.startsWith("/") - ? url.hostname.slice(1) - : url.hostname - } - } else { - let nsSuffix = url.hostname.split(".")[1] // Todo: support sub domain - let ns = nameServices[nsSuffix] - if (!ns) throw new Error("invalid name service") - // Todo: resolve name service - git3Address = null // ns parse address - - chainId = chainId || ns.chainId - repoName = url.pathname.slice(1) - } - chainId = chainId || 3334 - let sender = url.username || null - let storage - if (chainId == 3334) { - storage = new ETHStorage(repoName, chainId, { - git3Address, - sender, - }) - } else { - storage = new SLIStorage(repoName, chainId, { - git3Address, - sender, - }) - } - + const protocol = parseGit3URI(p.remoteUrl) + const storage = new protocol.storageClass(protocol) git = new Git(p, storage) return }, diff --git a/src/git3/index.ts b/src/git3/index.ts index e9c3caf..4e54380 100644 --- a/src/git3/index.ts +++ b/src/git3/index.ts @@ -1,234 +1,278 @@ -import { mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync, existsSync } from 'fs' -import { ethers } from 'ethers' -import { Command } from 'commander' -import bip39 from 'bip39' -import inquirer from 'inquirer' -import parse from 'parse-git-config' -import { importActions, generateActions } from './actions.js' -import abis from "../config/abis.js" +import { mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync } from "fs" +import { ethers } from "ethers" +import { Command } from "commander" +import bip39 from "bip39" +import inquirer from "inquirer" +import { importActions, generateActions } from "./actions.js" import network from "../config/evm-network.js" +import { getWallet, randomRPC } from "../common/wallet.js" +import { parseGit3URI } from "../common/git3-protocol.js" +import { TxManager } from "../common/tx-manager.js" const program = new Command() +program.name("git3").description("git3 mangement tool").version("0.1.0") + program - .name('git3') - .description('git3 mangement tool') - .version('0.1.0') - -program.command('generate') - .alias('gen') - .alias('new') - .description('generate a cryto wallet to use git3') - .action(() => { - inquirer.prompt(generateActions).then(answers => { - const { keyType, name } = answers - const walletType = keyType === 'private key' ? 'privateKey' : 'mnemonic' - - const keyPath = process.env.HOME + "/.git3/keys" - mkdirSync(keyPath, { recursive: true }) - - if (readdirSync(keyPath).includes(name)) { - console.error(`wallet ${name} already exists`) - return - } - - const mnemonic = bip39.generateMnemonic() - const wallet = keyType === 'private key' - ? ethers.Wallet.createRandom() - : ethers.Wallet.fromMnemonic(mnemonic) - - const content = `${walletType}\n${keyType === 'private key' ? wallet.privateKey : mnemonic}\n` - writeFileSync(`${keyPath}/${name}`, content) - return - }) - }) - -program.command('list', { isDefault: true }) - .alias('ls') - .description('list all wallets in user folder ~/.git3/keys') - .option('-r, --raw', 'output raw wallet data with pravate key / mnemonic') - .action(params => { - - const keyPath = process.env.HOME + "/.git3/keys" - mkdirSync(keyPath, { recursive: true }) - const wallets = readdirSync(keyPath) - - if (wallets.length === 0) { - console.log('No wallet found, you can generate one use ') - } - - wallets.forEach(file => { - const content = readFileSync(`${keyPath}/${file}`).toString() - - - if (params.raw) { - console.log(`[${file}]`) - console.log(` ${content.split('\n')[0]} - ${content.split('\n')[1]}`) - console.log('\t') - return - } - - console.log(`[${file}]`) - const [walletType, key] = content.split('\n') - const etherWallet = walletType === 'privateKey' - ? new ethers.Wallet(key) - : ethers.Wallet.fromMnemonic(key) - const address = etherWallet.address - console.log(`address: ${address}`) - console.log('\t') - }) - }) - -program.command('import') - .description('import a wallet from a private key or mnemonic') - .action(() => { - inquirer.prompt(importActions).then(answers => { - const { keyType, key, name } = answers - const walletType = keyType === 'private key' ? 'privateKey' : 'mnemonic' - const keyPath = process.env.HOME + "/.git3/keys" - mkdirSync(keyPath, { recursive: true }) - - if (readdirSync(keyPath).includes(name)) { - console.error(`wallet ${name} already exists`) - return - } - - const content = `${walletType}\n${key}\n` - writeFileSync(`${keyPath}/${name}`, content) - return - }) - }) - -program.command('delete') - .description('delete a wallet') - .action(() => { - const keyPath = process.env.HOME + "/.git3/keys" - mkdirSync(keyPath, { recursive: true }) - const wallets = readdirSync(keyPath) - - if (wallets.length === 0) { - console.error('No wallet found, you can generate one with `git3 generate`') - return - } - - inquirer.prompt([ - { - type: 'list', - name: 'wallet', - message: 'Select wallet to delete', - choices: wallets - } - ]).then(answers => { - const { wallet } = answers - rmSync(`${keyPath}/${wallet}`) + .command("generate") + .alias("gen") + .alias("new") + .description("generate a cryto wallet to use git3") + .action(() => { + inquirer.prompt(generateActions).then((answers) => { + const { keyType, name } = answers + const walletType = + keyType === "private key" ? "privateKey" : "mnemonic" + + const keyPath = process.env.HOME + "/.git3/keys" + mkdirSync(keyPath, { recursive: true }) + + if (readdirSync(keyPath).includes(name)) { + console.error(`wallet ${name} already exists`) + return + } + + const mnemonic = bip39.generateMnemonic() + const wallet = + keyType === "private key" + ? ethers.Wallet.createRandom() + : ethers.Wallet.fromMnemonic(mnemonic) + + const content = `${walletType}\n${ + keyType === "private key" ? wallet.privateKey : mnemonic + }\n` + writeFileSync(`${keyPath}/${name}`, content) + return + }) }) - }) - -program.command('create') - .argument('[wallet]', 'wallet to use', 'default') - .argument('[repo]', 'repo name to create') - .description('create a new repo') - .action((wallet, repo) => { - - const keyPath = process.env.HOME + "/.git3/keys" - mkdirSync(keyPath, { recursive: true }) - const content = readFileSync(`${keyPath}/${wallet}`).toString() - - const [walletType, key] = content.split('\n') - const provider = new ethers.providers.JsonRpcProvider('https://galileo.web3q.io:8545'); - - let etherWallet = walletType === 'privateKey' - ? new ethers.Wallet(key) - : ethers.Wallet.fromMnemonic(key) - - etherWallet = etherWallet.connect(provider) - let net = network[3334] - const contract = new ethers.Contract( - net.contracts.git3, - abis.ETHStorage, - etherWallet) +program + .command("list", { isDefault: true }) + .alias("ls") + .description("list all wallets in user folder ~/.git3/keys") + .option("-r, --raw", "output raw wallet data with pravate key / mnemonic") + .action((params) => { + const keyPath = process.env.HOME + "/.git3/keys" + mkdirSync(keyPath, { recursive: true }) + const wallets = readdirSync(keyPath) + + if (wallets.length === 0) { + console.log("No wallet found, you can generate one use ") + } - contract.repoNameToOwner(Buffer.from(repo)) - .then((res: any) => { console.log(res) }) - .catch((err: any) => { console.error(err) }) - contract.createRepo(Buffer.from(repo)) - .then((res: any) => { console.log(res) }) - .catch((err: any) => { console.error(err) }) + wallets.forEach((file) => { + const content = readFileSync(`${keyPath}/${file}`).toString() + + if (params.raw) { + console.log(`[${file}]`) + console.log( + ` ${content.split("\n")[0]} - ${content.split("\n")[1]}` + ) + console.log("\t") + return + } + + console.log(`[${file}]`) + const [walletType, key] = content.split("\n") + const etherWallet = + walletType === "privateKey" + ? new ethers.Wallet(key) + : ethers.Wallet.fromMnemonic(key) + const address = etherWallet.address + console.log(`address: ${address}`) + console.log("\t") + }) + }) - }) +program + .command("import") + .description("import a wallet from a private key or mnemonic") + .action(() => { + inquirer.prompt(importActions).then((answers) => { + const { keyType, key, name } = answers + const walletType = + keyType === "private key" ? "privateKey" : "mnemonic" + const keyPath = process.env.HOME + "/.git3/keys" + mkdirSync(keyPath, { recursive: true }) + + if (readdirSync(keyPath).includes(name)) { + console.error(`wallet ${name} already exists`) + return + } + + const content = `${walletType}\n${key}\n` + writeFileSync(`${keyPath}/${name}`, content) + return + }) + }) -program.command('info') - .argument('[wallet]', 'wallet you want to get info', 'default') - .description('get info of a wallet') - .action(wallet => { +program + .command("delete") + .description("delete a wallet") + .action(() => { + const keyPath = process.env.HOME + "/.git3/keys" + mkdirSync(keyPath, { recursive: true }) + const wallets = readdirSync(keyPath) + + if (wallets.length === 0) { + console.error( + "No wallet found, you can generate one with `git3 generate`" + ) + return + } - const keyPath = process.env.HOME + "/.git3/keys" - mkdirSync(keyPath, { recursive: true }) + inquirer + .prompt([ + { + type: "list", + name: "wallet", + message: "Select wallet to delete", + choices: wallets, + }, + ]) + .then((answers) => { + const { wallet } = answers + rmSync(`${keyPath}/${wallet}`) + }) + }) - const content = readFileSync(`${keyPath}/${wallet}`).toString() - const [walletType, key] = content.split('\n') - const provider = new ethers.providers.JsonRpcProvider('https://galileo.web3q.io:8545'); +program + .command("create") + .argument("", "ex: default@git3.w3q/repo_name") + .description("create a new repo") + .action(async (uri) => { + if (!uri.startsWith("git3://")) { + uri = "git3://" + uri + } + const protocol = parseGit3URI(uri) + let owner = await protocol.contract.repoNameToOwner( + Buffer.from(protocol.repoName) + ) + + if (owner != "0x0000000000000000000000000000000000000000") { + console.error(`repo ${protocol.repoName} already exists`) + return + } + console.log( + `creating repo ${protocol.repoName} on ${protocol.netConfig.name}...` + ) + const txManager = new TxManager( + protocol.contract, + protocol.chainId, + protocol.netConfig.txConst + ) + let receipt = await txManager.SendCall("createRepo", [ + Buffer.from(protocol.repoName), + ]) + if ( + protocol.netConfig.explorers && + protocol.netConfig.explorers.length > 0 + ) { + console.log( + protocol.netConfig.explorers[0].url.replace(/\/+$/, "") + + "/tx/" + + receipt.transactionHash + ) + } else { + console.log(receipt.transactionHash) + } + console.log(`repo ${protocol.repoName} created.`) + }) - let etherWallet = walletType === 'privateKey' - ? new ethers.Wallet(key) - : ethers.Wallet.fromMnemonic(key) +program + .command("info") + .argument("[wallet]", "wallet you want to get info", "default") + .description("get info of a wallet") + .action((wallet) => { + let etherWallet = getWallet(wallet) - etherWallet = etherWallet.connect(provider) - const address = etherWallet.address + const address = etherWallet.address - etherWallet.getBalance() - .then(balance => { console.log(`wallet: ${wallet}`) console.log(`address: ${address}`) - console.log(`balance: ${ethers.utils.formatUnits(balance)} eth`) - }) - .catch(err => { - console.error(err) - return - }) - }) - -program.command('set-wallet') - .alias('set') - .argument('', 'git3 remote') - .argument('[wallet]', 'wallet you want to bind', 'default') - .description('bind git3 remotes with a wallet') - .action((git3, wallet) => { - const currentConfig = parse.sync() - - const existingRemote = currentConfig[`remote "${git3}"`] - const keyPath = process.env.HOME + "/.git3/keys" - mkdirSync(keyPath, { recursive: true }) - - if (!existsSync(`${keyPath}/${wallet}`)) { - console.error(`wallet ${wallet} not found, use to generate one`) - return - } - - if (existingRemote) { - const newConfig = { - ...currentConfig, - [`remote "${git3}"`]: { - ...existingRemote, - wallet + + for (let [_, net] of Object.entries(network)) { + const provider = new ethers.providers.JsonRpcProvider( + randomRPC(net.rpc) + ) + const balance = provider.getBalance(address) + balance.then((res) => { + console.log( + `[${net.name}] balance: ${ethers.utils.formatUnits( + res, + net.nativeCurrency.decimals + )} ${net.nativeCurrency.symbol}` + ) + }) } - } - - // console.log(newConfig) - // const writer = createWriteStream('config', 'w') - let newConfigText = '' - Object.keys(newConfig).forEach(key => { - newConfigText += `[${key}]\n` - Object.keys(newConfig[key]).forEach(subKey => { - newConfigText += `\t${subKey} = ${newConfig[key][subKey]}\n` - }) - }) - let path = parse.resolveConfigPath("global") || "" - writeFileSync(path, newConfigText) - } else { - console.error(`remote ${git3} not found`) - console.error('you can add a remote with `git remote add ') - } - }) + }) + +program + .command("clear") + .description("clear pending nonce") + .argument("", "ex: default@git3.w3q") + .argument("[num]", "number of pending nonce to clear", 1) + .action(async (uri,num) => { + if (!uri.startsWith("git3://")) { + uri = "git3://" + uri + } + const protocol = parseGit3URI(uri, { skipRepoName: true }) + const txManager = new TxManager( + protocol.contract, + protocol.chainId, + protocol.netConfig.txConst + ) + let nonce = await protocol.wallet.getTransactionCount() + console.log(`current nonce: ${nonce}`) + await txManager.clearPendingNonce(num) + }) + +// Todo: set-wallet temporarily useless +// program +// .command("set-wallet") +// .alias("set") +// .argument("", "git3 remote") +// .argument("[wallet]", "wallet you want to bind", "default") +// .description("bind git3 remotes with a wallet") +// .action((git3, wallet) => { +// const currentConfig = parse.sync() + +// const existingRemote = currentConfig[`remote "${git3}"`] +// const keyPath = process.env.HOME + "/.git3/keys" +// mkdirSync(keyPath, { recursive: true }) + +// if (!existsSync(`${keyPath}/${wallet}`)) { +// console.error( +// `wallet ${wallet} not found, use to generate one` +// ) +// return +// } + +// if (existingRemote) { +// const newConfig = { +// ...currentConfig, +// [`remote "${git3}"`]: { +// ...existingRemote, +// wallet, +// }, +// } + +// // console.log(newConfig) +// // const writer = createWriteStream('config', 'w') +// let newConfigText = "" +// Object.keys(newConfig).forEach((key) => { +// newConfigText += `[${key}]\n` +// Object.keys(newConfig[key]).forEach((subKey) => { +// newConfigText += `\t${subKey} = ${newConfig[key][subKey]}\n` +// }) +// }) +// let path = parse.resolveConfigPath("global") || "" +// writeFileSync(path, newConfigText) +// } else { +// console.error(`remote ${git3} not found`) +// console.error( +// "you can add a remote with `git remote add " +// ) +// } +// }) program.parse() diff --git a/src/storage/ETHStorage.ts b/src/storage/ETHStorage.ts index 207d340..30c7353 100644 --- a/src/storage/ETHStorage.ts +++ b/src/storage/ETHStorage.ts @@ -1,34 +1,29 @@ import { Ref, Status, Storage } from "./storage.js" -import { getWallet } from "../wallet/index.js" -import { ethers, Signer } from "ethers" -import { NonceManager } from "@ethersproject/experimental" -import abis from "../config/abis.js" -import network from "../config/evm-network.js" +import { ethers } from "ethers" +import { TxManager } from "../common/tx-manager.js" +import { Git3Protocol } from "../common/git3-protocol.js" + export class ETHStorage implements Storage { repoName: string - wallet: Signer + wallet: ethers.Signer contract: ethers.Contract - provider: ethers.providers.JsonRpcProvider - - constructor(repoName: string, chainId: number, options: { git3Address: string | null, sender: string | null }) { - let net = network[chainId] - if (!net) throw new Error("chainId not supported") - - this.repoName = repoName - this.wallet = getWallet(options.sender) + txManager: TxManager - let rpc = net.rpc[Math.floor(Math.random() * net.rpc.length)] //random get rpc - - this.provider = new ethers.providers.JsonRpcProvider(rpc) - this.wallet = this.wallet.connect(this.provider) - this.wallet = new NonceManager(this.wallet) - - let repoAddress = options.git3Address || net.contracts.git3 - this.contract = new ethers.Contract(repoAddress, abis.ETHStorage, this.wallet) + constructor(protocol: Git3Protocol) { + this.repoName = protocol.repoName + this.contract = protocol.contract + this.wallet = protocol.wallet + this.txManager = new TxManager( + this.contract, + protocol.chainId, + protocol.netConfig.txConst + ) } async repoRoles(): Promise { - let owner = await this.contract.repoNameToOwner(Buffer.from(this.repoName)) + let owner = await this.contract.repoNameToOwner( + Buffer.from(this.repoName) + ) if (owner === ethers.constants.AddressZero) return [] return [owner] } @@ -39,28 +34,31 @@ export class ETHStorage implements Storage { } async download(path: string): Promise<[Status, Buffer]> { - const res = await this.contract.download(Buffer.from(this.repoName), Buffer.from(path)) - const buffer = Buffer.from(res[0].slice(2), 'hex') - console.error(`=== download file ${path} result ===`) - // console.error(buffer.toString('utf-8')) + const res = await this.contract.download( + Buffer.from(this.repoName), + Buffer.from(path) + ) + const buffer = Buffer.from(res[0].slice(2), "hex") + console.error(`=== download file ${path} succeed ===`) return [Status.SUCCEED, buffer] } async upload(path: string, file: Buffer): Promise { try { console.error(`=== uploading file ${path} ===`) - const tx = await this.contract.upload(Buffer.from(this.repoName), Buffer.from(path), file, { gasLimit: 6000000 }) - console.error(`send tx done: ${tx.hash}`) - await new Promise(r => setTimeout(r, 3000)) - await tx.wait(1) - console.error(`upload succeed: ${tx.hash}`) + await this.txManager.SendCall("upload", [ + Buffer.from(this.repoName), + Buffer.from(path), + file, + ]) + console.error(`=== upload ${path} succeed ===`) + return Status.SUCCEED - } - catch (error: any) { - console.error(`upload failed ${error.reason ? error.reason : "CALL_EXCEPTION"} : ${path}`) + } catch (error: any) { + this.txManager.CancelAll() + console.error(`upload failed: ${error}`) return Status.FAILED } - } remove(path: string): Promise { @@ -68,29 +66,40 @@ export class ETHStorage implements Storage { } async listRefs(): Promise { - const res: string[][] = await this.contract.listRefs(Buffer.from(this.repoName)) - let refs = res.map(i => ({ - ref: Buffer.from(i[1].slice(2), "hex").toString("utf8").slice(this.repoName.length + 1), - sha: i[0].slice(2) + const res: string[][] = await this.contract.listRefs( + Buffer.from(this.repoName) + ) + let refs = res.map((i) => ({ + ref: Buffer.from(i[1].slice(2), "hex") + .toString("utf8") + .slice(this.repoName.length + 1), + sha: i[0].slice(2), })) return refs } async setRef(path: string, sha: string): Promise { try { - let tx = await this.contract.setRef(Buffer.from(this.repoName), Buffer.from(path), '0x' + sha, { gasLimit: 6000000 }) - await new Promise(r => setTimeout(r, 1000)) - await tx.wait(1) - } - catch (error: any) { - console.error(`ref set failed ${error.reason ? error.reason : "CALL_EXCEPTION"} : ${path}`) + console.error(`=== setting ref ${path} ===`) + await this.txManager.SendCall("setRef", [ + Buffer.from(this.repoName), + Buffer.from(path), + "0x" + sha, + ]) + + console.error(`ref set succeed ${path}`) + return Status.SUCCEED + } catch (error: any) { + console.error(`ref set failed ${error} : ${path}`) return Status.FAILED } - return Status.SUCCEED } async removeRef(path: string): Promise { - await this.contract.delRef(Buffer.from(this.repoName), Buffer.from(path)) + await this.contract.delRef( + Buffer.from(this.repoName), + Buffer.from(path) + ) return Status.SUCCEED } } diff --git a/src/storage/SLIStorage.ts b/src/storage/SLIStorage.ts index 5426270..d69bf46 100644 --- a/src/storage/SLIStorage.ts +++ b/src/storage/SLIStorage.ts @@ -1,49 +1,35 @@ import { Ref, Status, Storage } from "./storage.js" -import { getWallet } from "../wallet/index.js" -import { TxManager } from "../wallet/tx-manager.js" -import { ethers, Signer } from "ethers" -import { NonceManager } from "@ethersproject/experimental" -import abis from "../config/abis.js" -import network from "../config/evm-network.js" +import { TxManager } from "../common/tx-manager.js" +import { ethers } from "ethers" import ipfsConf from "../config/ipfs.js" import axios from "axios" +import { Git3Protocol } from "../common/git3-protocol.js" export class SLIStorage implements Storage { repoName: string - wallet: Signer + wallet: ethers.Wallet contract: ethers.Contract - provider: ethers.providers.JsonRpcProvider - auth: string - txManager: TxManager - constructor( - repoName: string, - chainId: number, - options: { git3Address: string | null; sender: string | null } - ) { - let net = network[chainId] - if (!net) throw new Error("chainId not supported") - - this.repoName = repoName - this.wallet = getWallet(options.sender) - - let rpc = net.rpc[Math.floor(Math.random() * net.rpc.length)] //random get rpc - - this.provider = new ethers.providers.JsonRpcProvider(rpc) - this.wallet = this.wallet.connect(this.provider) - // this.wallet = new NonceManager(this.wallet) + auth: string - let repoAddress = options.git3Address || net.contracts.git3 - this.contract = new ethers.Contract( - repoAddress, - abis.SLIStorage, - this.wallet + constructor(protocol: Git3Protocol) { + this.repoName = protocol.repoName + this.contract = protocol.contract + this.wallet = protocol.wallet + this.txManager = new TxManager( + this.contract, + protocol.chainId, + protocol.netConfig.txConst ) this.auth = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweGFEQTdCOWFlQTdGNTc2ZDI5NzM0ZWUxY0Q2ODVFMzc2OWNCM2QwRDEiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTY3NTQ5NDYwMDkzMiwibmFtZSI6ImZ2bS1oYWNrc29uIn0.YBqfsj_LTZSJPKc0OH586avnQNqove_Htzl5rrToXTk" - this.txManager = new TxManager(this.contract, chainId, net.txConst) + this.txManager = new TxManager( + this.contract, + protocol.chainId, + protocol.netConfig.txConst + ) } async repoRoles(): Promise { @@ -98,7 +84,7 @@ export class SLIStorage implements Storage { Buffer.from(path), Buffer.from(cid), ]) - console.error(`=== upload ${path} ${cid} succeed ===`) + console.error(`=== upload ${path} ${cid.slice(0, 6)} succeed ===`) return Status.SUCCEED } catch (error: any) { @@ -150,9 +136,10 @@ export class SLIStorage implements Storage { } async storeIPFS(data: Buffer): Promise { + const RETRY_TIMES = 10 + const TIMEOUT = 30 let response - for (let i = 0; i < 10; i++) { - // Todo: add timeout + for (let i = 0; i < RETRY_TIMES; i++) { try { response = await axios.post( "https://api.nft.storage/upload", @@ -162,6 +149,7 @@ export class SLIStorage implements Storage { "Content-Type": "application/octet-stream", Authorization: this.auth, }, + timeout: TIMEOUT * 1000, } ) if (response.status == 200) { diff --git a/src/wallet/index.ts b/src/wallet/index.ts deleted file mode 100644 index 27b38e9..0000000 --- a/src/wallet/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { mkdirSync, readFileSync } from "fs" -import { ethers } from 'ethers' - -export function getWallet(sender: string | null): ethers.Wallet { - // Todo: according sender address to select wallet, if sender==null then use default wallet - const wallet = 'default' - - const keyPath = process.env.HOME + "/.git3/keys" - mkdirSync(keyPath, { recursive: true }) - - const content = readFileSync(`${keyPath}/${wallet}`).toString() - const [walletType, key] = content.split('\n') - - let etherWallet = walletType === 'privateKey' - ? new ethers.Wallet(key) - : ethers.Wallet.fromMnemonic(key) - - return etherWallet -}