|
@@ -1,23 +1,49 @@
|
|
|
-import {
|
|
|
- forEachAsync,
|
|
|
- generateRandomString,
|
|
|
- getAxiosClient,
|
|
|
- getProxyAgent,
|
|
|
-} from '../utils'
|
|
|
-import { getAltchaPayload, getRecaptcha, solveHCaptcha } from './captcha'
|
|
|
+import { ethers } from 'ethers'
|
|
|
+import { forEachAsync, getAxiosClient } from '../utils'
|
|
|
+import { getAltchaPayload } from './captcha'
|
|
|
import { DBClient } from '../singletons'
|
|
|
-import { Status } from '../models/Status'
|
|
|
+import { InitiaClient } from '../InitiaClient'
|
|
|
+import { Faucet } from '@prisma/client'
|
|
|
import polly from 'polly-js'
|
|
|
-import { MnemonicKey } from '@initia/initia.js'
|
|
|
-import { ethers } from 'ethers'
|
|
|
-import { v4 as uuidv4 } from 'uuid'
|
|
|
-import axios from 'axios'
|
|
|
-import xrpl from 'xrpl'
|
|
|
+import cron from 'node-cron'
|
|
|
|
|
|
-export async function faucetAccount(address: string) {
|
|
|
+export async function faucetAccount(address: string, passportPK: string) {
|
|
|
+ const passportWallet = new ethers.Wallet(passportPK)
|
|
|
const client = getAxiosClient(true)
|
|
|
+ const statusResp = await client.get(`
|
|
|
+https://faucet-api.initiation-1.initia.xyz/status/${passportWallet.address}`)
|
|
|
+ if (statusResp.data.passport_score < 20) {
|
|
|
+ // 分数不够一天后重试
|
|
|
+ await DBClient.instance.faucet.updateMany({
|
|
|
+ where: { passportPrivateKey: passportPK },
|
|
|
+ data: {
|
|
|
+ availableFrom: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
|
|
|
+ },
|
|
|
+ })
|
|
|
+ throw new Error(`score ${statusResp.data.passport_score} not qualified`)
|
|
|
+ }
|
|
|
+ if (statusResp.data.ms_before_next > 0) {
|
|
|
+ // 在 cd 中重置 cd 时间
|
|
|
+ await DBClient.instance.faucet.updateMany({
|
|
|
+ where: { passportPrivateKey: passportPK },
|
|
|
+ data: {
|
|
|
+ availableFrom: new Date(
|
|
|
+ new Date().getTime() + statusResp.data.ms_before_next,
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ })
|
|
|
+ throw new Error(`still in cooldown: ${statusResp.data.ms_before_next}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ const passportChallengeResp = await client.post(
|
|
|
+ 'https://faucet-api.initiation-1.initia.xyz/create_passport_message',
|
|
|
+ { passport_address: passportWallet.address },
|
|
|
+ )
|
|
|
+ const passportSignature = await passportWallet.signMessage(
|
|
|
+ passportChallengeResp.data.message,
|
|
|
+ )
|
|
|
+
|
|
|
const altcha = await getAltchaPayload(client)
|
|
|
- const hCaptcha = await solveHCaptcha(address)
|
|
|
try {
|
|
|
const res = await client.post(
|
|
|
`https://faucet-api.initiation-1.initia.xyz/claim`,
|
|
@@ -25,7 +51,9 @@ export async function faucetAccount(address: string) {
|
|
|
address: address,
|
|
|
altcha_payload: altcha,
|
|
|
denom: 'uinit',
|
|
|
- h_captcha: hCaptcha,
|
|
|
+ discord_code: '',
|
|
|
+ passport_address: passportWallet.address,
|
|
|
+ passport_signature: passportSignature,
|
|
|
},
|
|
|
)
|
|
|
console.log(JSON.stringify(res.data))
|
|
@@ -35,334 +63,126 @@ export async function faucetAccount(address: string) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function startFaucet(concurrency, index) {
|
|
|
- const accountsRaw = await DBClient.instance.account.findMany({
|
|
|
- where: {
|
|
|
- status: Status.Inited,
|
|
|
- },
|
|
|
+async function checkFaucetBalance() {
|
|
|
+ const accounts = await DBClient.instance.faucet.findMany({
|
|
|
+ where: { balance: 0 },
|
|
|
})
|
|
|
- const accounts = accountsRaw.filter(account => account.id % 10 === index)
|
|
|
- await forEachAsync(accounts, concurrency, async (account, index) => {
|
|
|
- console.log(`${index}/${accounts.length}: processing ${account.address}`)
|
|
|
+ await forEachAsync(accounts, 2, async (account, index) => {
|
|
|
try {
|
|
|
- await faucetAccount(account.address)
|
|
|
-
|
|
|
- await DBClient.instance.account.update({
|
|
|
+ console.log(`checking ${index}/${accounts.length}`)
|
|
|
+ const client = new InitiaClient(account.mnemonic, true)
|
|
|
+ const gasAmount = await client.getGasAmount()
|
|
|
+ console.log(index, gasAmount)
|
|
|
+ await DBClient.instance.faucet.update({
|
|
|
where: { id: account.id },
|
|
|
- data: { status: Status.Fauceted },
|
|
|
+ data: { balance: gasAmount },
|
|
|
})
|
|
|
} catch (e) {
|
|
|
- console.log(e)
|
|
|
- await DBClient.instance.account.update({
|
|
|
- where: { id: account.id },
|
|
|
- data: { status: Status.FaucetFailed, message: e.message },
|
|
|
- })
|
|
|
+ console.log(`${index}: error`)
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function getAndroidModel() {
|
|
|
- const models = [
|
|
|
- 'Mi 9',
|
|
|
- 'LIO-AN00',
|
|
|
- 'PCRT00',
|
|
|
- 'V1824A',
|
|
|
- 'SM-N9760',
|
|
|
- 'HD1900',
|
|
|
- 'SKW-A0',
|
|
|
- 'NX629J',
|
|
|
- 'M973Q',
|
|
|
- 'PCLM10',
|
|
|
- 'SM-N9810',
|
|
|
- 'SM-N9860',
|
|
|
- 'SM-F9160',
|
|
|
- 'SM-A7009',
|
|
|
- 'SM-A6060',
|
|
|
- 'SM-W2018',
|
|
|
- 'SM-P610',
|
|
|
- 'VTR-L29',
|
|
|
- 'M1903F2G',
|
|
|
- 'XQ-BE72',
|
|
|
- 'RMX3709',
|
|
|
- 'SM-E5260',
|
|
|
- 'SM-W2020',
|
|
|
- 'SHARK KSR-A0',
|
|
|
- 'G8HNN',
|
|
|
- 'SM-G973U',
|
|
|
- 'SM-G973U1',
|
|
|
- 'SM-G9738',
|
|
|
- 'SM-G981B',
|
|
|
- 'SM-G988U',
|
|
|
- 'SM-G9880',
|
|
|
- 'M2012K11G',
|
|
|
- 'M2006C3LG',
|
|
|
- '21061119BI',
|
|
|
- '22127RK46C',
|
|
|
- 'M2102J20SI',
|
|
|
- 'MCE91',
|
|
|
- 'L16',
|
|
|
- 'L10',
|
|
|
- 'L12',
|
|
|
- 'L13',
|
|
|
- 'M11A',
|
|
|
- 'N11',
|
|
|
- 'N12',
|
|
|
- ]
|
|
|
- return models[Math.floor(Math.random() * models.length)]
|
|
|
-}
|
|
|
-
|
|
|
-function getAndroidOS() {
|
|
|
- const oses = ['9', '10', '11', '12', '12.1', '13']
|
|
|
- return `Android ${oses[Math.floor(Math.random() * oses.length)]}`
|
|
|
-}
|
|
|
-
|
|
|
-function getIOS() {
|
|
|
- const iOSes = [
|
|
|
- '14',
|
|
|
- '16.7.8',
|
|
|
- '17.5',
|
|
|
- '17.4.1',
|
|
|
- '16.7.7',
|
|
|
- '15.8.2',
|
|
|
- '17.3.1',
|
|
|
- '15.0.2',
|
|
|
- '15.1.1',
|
|
|
- '16.0.3',
|
|
|
- '16.4.1',
|
|
|
- ]
|
|
|
- return `iOS ${iOSes[Math.floor(Math.random() * iOSes.length)]}`
|
|
|
-}
|
|
|
+function shuffleArray(array) {
|
|
|
+ for (let i = array.length - 1; i > 0; i--) {
|
|
|
+ // 生成一个随机索引
|
|
|
+ const j = Math.floor(Math.random() * (i + 1))
|
|
|
|
|
|
-function formatDateTime(date) {
|
|
|
- function pad(number, length) {
|
|
|
- return number.toString().padStart(length, '0')
|
|
|
+ // 交换当前元素与随机选中的元素
|
|
|
+ ;[array[i], array[j]] = [array[j], array[i]]
|
|
|
}
|
|
|
-
|
|
|
- const offset = -date.getTimezoneOffset()
|
|
|
- const offsetSign = offset >= 0 ? '+' : '-'
|
|
|
- const offsetHours = pad(Math.floor(Math.abs(offset) / 60), 2)
|
|
|
- const offsetMinutes = pad(Math.abs(offset) % 60, 2)
|
|
|
-
|
|
|
- return (
|
|
|
- date.getFullYear() +
|
|
|
- '-' +
|
|
|
- pad(date.getMonth() + 1, 2) +
|
|
|
- '-' +
|
|
|
- pad(date.getDate(), 2) +
|
|
|
- 'T' +
|
|
|
- pad(date.getHours(), 2) +
|
|
|
- ':' +
|
|
|
- pad(date.getMinutes(), 2) +
|
|
|
- ':' +
|
|
|
- pad(date.getSeconds(), 2) +
|
|
|
- '.' +
|
|
|
- pad(date.getMilliseconds(), 3) +
|
|
|
- offsetSign +
|
|
|
- offsetHours +
|
|
|
- offsetMinutes
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-async function getDeviceId(model: string, httpsAgent): Promise<string> {
|
|
|
- const client = axios.create({
|
|
|
- httpsAgent: httpsAgent,
|
|
|
- headers: {
|
|
|
- 'X-Android-Package': 'xyz.lunchlunch.app',
|
|
|
- 'x-firebase-client':
|
|
|
- 'H4sIAAAAAAAAAKtWykhNLCpJSk0sKVayio7VUSpLLSrOzM9TslIyUqoFAFyivEQfAAAA',
|
|
|
- 'X-Android-Cert': '841CB3E1F4FC6CD420202DD419E02D4EE2E2099B',
|
|
|
- 'x-goog-api-key': 'AIzaSyA1PYciMbJ03xonKRM3JBr4yTReQ67GeuU',
|
|
|
- 'User-Agent': `Dalvik/2.1.0 (Linux; U; Android 9; ${model} Build/PQ3B.190801.05281822)`,
|
|
|
- },
|
|
|
- })
|
|
|
- const deviceId = generateRandomString(22)
|
|
|
- await client.post(
|
|
|
- 'https://firebaseinstallations.googleapis.com/v1/projects/lunchlunch-fb95e/installations',
|
|
|
- {
|
|
|
- fid: deviceId,
|
|
|
- appId: '1:383319257117:android:1a7c776df5c7048bddd6f4',
|
|
|
- authVersion: 'FIS_v2',
|
|
|
- sdkVersion: 'a:18.0.0',
|
|
|
- },
|
|
|
- )
|
|
|
- return deviceId
|
|
|
+ return array
|
|
|
}
|
|
|
|
|
|
-async function getRawMessage(address: string, message: string, duid: string) {
|
|
|
- const resp = await axios.post('https://rem.mtdao.io/create', {
|
|
|
- address,
|
|
|
- message,
|
|
|
- duid,
|
|
|
- })
|
|
|
- return resp.data
|
|
|
+async function getNeedFaucetAccounts(limit: number) {
|
|
|
+ const now = new Date()
|
|
|
+ return DBClient.instance.$queryRaw<Faucet[]>`
|
|
|
+ SELECT *
|
|
|
+ FROM Faucet as f1
|
|
|
+ WHERE (f1.passportPrivateKey, f1.lastRun) IN
|
|
|
+ (SELECT passportPrivateKey, MIN(lastRun) AS lastRun
|
|
|
+ FROM Faucet
|
|
|
+ WHERE availableFrom < ${now}
|
|
|
+ GROUP BY passportPrivateKey)
|
|
|
+ ORDER BY f1.balance asc
|
|
|
+ LIMIT ${limit}
|
|
|
+ `
|
|
|
}
|
|
|
|
|
|
-async function faucetLunch(concurrency: number = 8) {
|
|
|
- const now = new Date()
|
|
|
- const accounts = await DBClient.instance.account.findMany({
|
|
|
- where: {
|
|
|
- lastRun: {
|
|
|
- lt: new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
|
|
- },
|
|
|
- status: 0,
|
|
|
- },
|
|
|
- orderBy: {
|
|
|
- id: 'desc',
|
|
|
- },
|
|
|
- })
|
|
|
- await forEachAsync(accounts, concurrency, async (account, index) => {
|
|
|
- console.log(`${index}/${accounts.length}: processing ${account.address}`)
|
|
|
+async function startFaucet(concurrency: number) {
|
|
|
+ const accounts = await getNeedFaucetAccounts(300)
|
|
|
+ // shuffleArray(accounts)
|
|
|
+ if (accounts.length === 0) {
|
|
|
+ console.log('no account to faucet')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await forEachAsync<Faucet>(accounts, concurrency, async (account, index) => {
|
|
|
+ console.log(
|
|
|
+ `${index}/${accounts.length}: processing ${account.id}: ${account.address}`,
|
|
|
+ )
|
|
|
try {
|
|
|
- if (!account.gmail) {
|
|
|
- account.gmail = `${generateRandomString(8)}@gmail.com`
|
|
|
- }
|
|
|
- if (!account.xrpPrivateKey) {
|
|
|
- account.xrpPrivateKey = xrpl.Wallet.generate().seed
|
|
|
- }
|
|
|
-
|
|
|
- const key = new MnemonicKey({ mnemonic: account.mnemonic })
|
|
|
- const evmWallet = new ethers.Wallet(key.privateKey.toString('hex'))
|
|
|
- const message = JSON.stringify({
|
|
|
- signedAt: formatDateTime(new Date()),
|
|
|
- })
|
|
|
- const evmSignedMessage = await evmWallet.signMessage(message)
|
|
|
- const proxyAgent = getProxyAgent()
|
|
|
- let isAndroid = Math.random() > 0.5
|
|
|
- if (account.duid) {
|
|
|
- isAndroid = !account.duid.includes('-')
|
|
|
- }
|
|
|
- if (isAndroid && !account.deviceModel) {
|
|
|
- account.deviceModel = getAndroidModel()
|
|
|
- }
|
|
|
- if (!account.duid) {
|
|
|
- account.duid = isAndroid
|
|
|
- ? await getDeviceId(account.deviceModel, proxyAgent)
|
|
|
- : uuidv4().toUpperCase()
|
|
|
- }
|
|
|
- if (!account.deviceOS) {
|
|
|
- account.deviceOS = isAndroid ? getAndroidOS() : getIOS()
|
|
|
- }
|
|
|
- const rawMessage = await getRawMessage(
|
|
|
- evmWallet.address,
|
|
|
- message,
|
|
|
- account.duid,
|
|
|
- )
|
|
|
- const client = isAndroid
|
|
|
- ? axios.create({
|
|
|
- headers: {
|
|
|
- 'Lunch-Language': 'EN',
|
|
|
- 'lunch-fiat-currency': 'USD',
|
|
|
- 'lunch-app-duid': account.duid,
|
|
|
- 'lunch-app-version': '0.17.3',
|
|
|
- 'lun-app-build': 46,
|
|
|
- 'Lunch-Device-Model': account.deviceModel,
|
|
|
- 'Lunch-Device-OS': account.deviceOS,
|
|
|
- 'Lunch-Platform': 'Android',
|
|
|
- 'user-agent': `lunch/0.17.3(46) (Linux; ${account.deviceOS}; ${account.deviceModel} Build/PQ3B.190801.05281822)`,
|
|
|
- },
|
|
|
- httpsAgent: proxyAgent,
|
|
|
- })
|
|
|
- : axios.create({
|
|
|
- headers: {
|
|
|
- 'lunch-language': 'en',
|
|
|
- 'lunch-app-platform': 'iOS',
|
|
|
- 'lunch-app-version': '44',
|
|
|
- 'lunch-fiat-currency': 'USD',
|
|
|
- 'lunch-app-duid': account.duid,
|
|
|
- 'user-agent': `Lunch/1.0 (xyz.lunchlunch.app; build:44; ${account.deviceOS}) Alamofire/5.8.0`,
|
|
|
- },
|
|
|
- })
|
|
|
-
|
|
|
- const resp = await client.post(
|
|
|
- 'https://api.lunchlunch.xyz/v1/auth/sign-in',
|
|
|
- {
|
|
|
- walletAddress: evmWallet.address,
|
|
|
- signedMessage: evmSignedMessage,
|
|
|
- rawMessage: rawMessage,
|
|
|
- },
|
|
|
- )
|
|
|
- console.log(`${index}: sign-in success.`)
|
|
|
- client.defaults.headers[
|
|
|
- 'authorization'
|
|
|
- ] = `Bearer ${resp.data.accessToken}`
|
|
|
-
|
|
|
- const xrplWallet = xrpl.Wallet.fromSeed(account.xrpPrivateKey)
|
|
|
- let needRegister = false
|
|
|
- try {
|
|
|
- const memberResp = await client.get(
|
|
|
- 'https://api.lunchlunch.xyz/v1/member',
|
|
|
- )
|
|
|
- } catch (e) {
|
|
|
- if (e.response.status === 404) {
|
|
|
- needRegister = true
|
|
|
- }
|
|
|
- }
|
|
|
- if (needRegister) {
|
|
|
- const register = await client.post(
|
|
|
- 'https://api.lunchlunch.xyz/v1/member/register',
|
|
|
- {
|
|
|
- evmWalletAddress: evmWallet.address,
|
|
|
- initiaWalletAddress: key.accAddress,
|
|
|
- isImportedWallet: true,
|
|
|
- firstInitiaWalletAddress: key.accAddress,
|
|
|
- email: account.gmail,
|
|
|
- xrplWalletAddress: xrplWallet.address,
|
|
|
- },
|
|
|
- )
|
|
|
- console.log(`${index}: register done`)
|
|
|
- }
|
|
|
+ await faucetAccount(account.address, account.passportPrivateKey)
|
|
|
+ // 对 db 的更新加上 retry 防止偶发的db 连接错误
|
|
|
await polly()
|
|
|
- .waitAndRetry([2000, 2000, 3000, 3000, 3000])
|
|
|
+ .waitAndRetry(2)
|
|
|
.executeForPromise(async () => {
|
|
|
- await client.post(
|
|
|
- 'https://api.lunchlunch.xyz/v1/dish/submit-action/airdrop',
|
|
|
- {
|
|
|
- dishId: 43,
|
|
|
+ // 更新领过水的条目的 lastRun
|
|
|
+ await DBClient.instance.faucet.update({
|
|
|
+ where: { id: account.id },
|
|
|
+ data: { lastRun: new Date() },
|
|
|
+ })
|
|
|
+ // 更新同 passportPK 下所有的 availableFrom
|
|
|
+ await DBClient.instance.faucet.updateMany({
|
|
|
+ where: { passportPrivateKey: account.passportPrivateKey },
|
|
|
+ data: {
|
|
|
+ availableFrom: new Date(
|
|
|
+ new Date().getTime() + 24 * 60 * 60 * 1000,
|
|
|
+ ),
|
|
|
},
|
|
|
- )
|
|
|
- console.log(`${index}: airdrop done`)
|
|
|
+ })
|
|
|
})
|
|
|
-
|
|
|
- await DBClient.instance.account.update({
|
|
|
- where: { id: account.id },
|
|
|
- data: {
|
|
|
- status: Status.Fauceted,
|
|
|
- lastRun: new Date(),
|
|
|
- xrpPrivateKey: account.xrpPrivateKey,
|
|
|
- gmail: account.gmail,
|
|
|
- duid: account.duid,
|
|
|
- deviceModel: account.deviceModel,
|
|
|
- deviceOS: account.deviceOS,
|
|
|
- },
|
|
|
- })
|
|
|
+ console.log(
|
|
|
+ `${index}/${accounts.length}: successfully faucet ${account.id}: ${account.address}`,
|
|
|
+ )
|
|
|
} catch (e) {
|
|
|
- console.log(e)
|
|
|
- await DBClient.instance.account.update({
|
|
|
+ console.error(
|
|
|
+ `${index}/${accounts.length}: error occurred on ${account.id}: ${account.address}`,
|
|
|
+ e,
|
|
|
+ )
|
|
|
+ // 记录错误信息,更新 lastRun(出错跳过这一轮)
|
|
|
+ await DBClient.instance.faucet.update({
|
|
|
where: { id: account.id },
|
|
|
- data: {
|
|
|
- status: Status.FaucetFailed,
|
|
|
- message: e.message,
|
|
|
- lastRun: new Date(),
|
|
|
- xrpPrivateKey: account.xrpPrivateKey,
|
|
|
- gmail: account.gmail,
|
|
|
- duid: account.duid,
|
|
|
- deviceModel: account.deviceModel,
|
|
|
- deviceOS: account.deviceOS,
|
|
|
- },
|
|
|
+ data: { message: e.message, lastRun: new Date() },
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-await faucetLunch(8)
|
|
|
-//
|
|
|
-// const phase =
|
|
|
-// 'leave bone supply chair brain thunder giant fatigue winter shrimp father stairs'
|
|
|
-// const wallet = ethers.Wallet.fromPhrase(phase)
|
|
|
-// console.log(wallet.address)
|
|
|
-// const key = new MnemonicKey({
|
|
|
-// mnemonic: phase,
|
|
|
-// })
|
|
|
-// const rawMessage =
|
|
|
-// '809a55a47f136226b26cd6f12e61c4b641895e95f7eb43851005884cd8102bac2e231d14775fa9034745491d08910a1c0d0799ddf6926397dd05e733f62bcbb4'
|
|
|
-// const signedAmino = key.createSignatureAmino()
|
|
|
+let isRunning = false
|
|
|
+function startCron(concurrency: number) {
|
|
|
+ const queueTask = cron.schedule(
|
|
|
+ '* * * * *',
|
|
|
+ async () => {
|
|
|
+ if (isRunning) return
|
|
|
+ try {
|
|
|
+ isRunning = true
|
|
|
+ await startFaucet(concurrency)
|
|
|
+ } catch (e) {
|
|
|
+ console.log('cron error', e)
|
|
|
+ } finally {
|
|
|
+ isRunning = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ runOnInit: true,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ // stop cron job when server is stopped
|
|
|
+ process.on('SIGINT', () => {
|
|
|
+ queueTask.stop()
|
|
|
+ process.exit(0)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+startCron(2)
|
|
|
+// await checkFaucetBalance()
|