ソースを参照

Merge remote-tracking branch 'origin/main'

# Conflicts:
#	src/BaseClient.ts
hel 1 年間 前
コミット
3e0d488180
8 ファイル変更512 行追加328 行削除
  1. 3 1
      package.json
  2. 12 1
      prisma/schema.prisma
  3. 2 2
      src/faucet/captcha.ts
  4. 140 320
      src/faucet/faucet.ts
  5. 342 0
      src/faucet/faucetLunch.ts
  6. 0 1
      src/test.ts
  7. 2 2
      src/utils/index.ts
  8. 11 1
      yarn.lock

+ 3 - 1
package.json

@@ -4,7 +4,7 @@
   "description": "Minimalistic boilerplate to quick-start Node.js development in TypeScript.",
   "type": "module",
   "engines": {
-    "node": ">= 18.12"
+    "node": ">= 18.8"
   },
   "devDependencies": {
     "@babel/cli": "^7.24.5",
@@ -12,6 +12,8 @@
     "@babel/preset-env": "^7.24.5",
     "@types/jest": "~29.5",
     "@types/node": "~18",
+    "@types/node-cron": "^3.0.11",
+    "@types/uuid": "^9.0.8",
     "@typescript-eslint/eslint-plugin": "~6.2",
     "@typescript-eslint/parser": "~6.2",
     "babelify": "^10.0.0",

+ 12 - 1
prisma/schema.prisma

@@ -22,12 +22,23 @@ model Account {
   gmail         String?  @db.VarChar(100)
   status        Int      @default(0)
   message       String?  @db.Text
-  lastRun       DateTime @db.DateTime()
+  lastRun       DateTime @db.DateTime(0)
   duid          String?  @db.VarChar(80)
   deviceModel   String?  @db.VarChar(30)
   deviceOS      String?  @db.VarChar(30)
 }
 
+model Faucet {
+  id                 Int      @id @default(autoincrement())
+  mnemonic           String   @db.VarChar(255)
+  address            String   @db.VarChar(100)
+  balance            BigInt?
+  passportPrivateKey String   @db.VarChar(100)
+  lastRun            DateTime @db.DateTime(0)
+  availableFrom      DateTime @db.DateTime(0)
+  message            String?  @db.Text
+}
+
 model RandomTask {
   id       Int    @id @default(autoincrement())
   mnemonic String @db.VarChar(255)

+ 2 - 2
src/faucet/captcha.ts

@@ -41,8 +41,8 @@ export async function getRecaptcha(address: string) {
 export async function getAltchaPayload(client: AxiosInstance) {
   return await polly()
     .waitAndRetry(5)
-    .executeForPromise(async () => {
-      // console.log('retry:', info.count)
+    .executeForPromise(async info => {
+      console.log('altcha retry:', info.count)
       const challengeResp = await client.get(
         'https://faucet-api.initiation-1.initia.xyz/create_challenge',
       )

+ 140 - 320
src/faucet/faucet.ts

@@ -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()

+ 342 - 0
src/faucet/faucetLunch.ts

@@ -0,0 +1,342 @@
+import { forEachAsync, generateRandomString } from '../utils'
+import { DBClient } from '../singletons'
+import { Status } from '../models/Status'
+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 { InitiaClient } from '../InitiaClient'
+
+
+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 formatDateTime(date) {
+  function pad(number, length) {
+    return number.toString().padStart(length, '0')
+  }
+
+  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(
+  isAndroid: boolean,
+  model: string,
+  httpsAgent,
+): Promise<string> {
+  const deviceId = generateRandomString(22)
+  let client
+  if (isAndroid) {
+    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)`,
+      },
+    })
+  } else {
+  }
+  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
+}
+
+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 faucetLunch(concurrency: number = 8) {
+  const now = new Date()
+  const accounts = await DBClient.instance.account.findMany({
+    where: {
+      // address: 'init1muxa5dedmr2wcc9dkd4k8d54al9gsu5szyw9rd',
+      lastRun: {
+        lt: new Date(now.getTime() - 24 * 60 * 60 * 1000),
+      },
+      status: 0,
+    },
+    orderBy: {
+      id: 'asc',
+    },
+  })
+  await forEachAsync(accounts, concurrency, async (account, index) => {
+    console.log(`${index}/${accounts.length}: processing ${account.address}`)
+    account.gmail = 'cryptoky1998@gmail.com'
+    account.duid = 'FBF66888-C389-4C27-B832-CB49A923CED6'
+    // account.mnemonic= 'can similar vanish left sudden vocal plate acoustic humor toast observe tortoise demand cook ankle planet angle future detail scout rookie flock type top'
+    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 key = new MnemonicKey()
+      // account.duid = uuidv4().toUpperCase()
+      // const evmWallet = new ethers.Wallet(key.privateKey.toString('hex'))
+      const evmWallet = ethers.Wallet.fromPhrase(account.mnemonic)
+      // const evmWallet = ethers.Wallet.createRandom()
+      const message = JSON.stringify({
+        signedAt: formatDateTime(new Date()),
+      })
+      const evmSignedMessage = await evmWallet.signMessage(message)
+      const proxyAgent = undefined
+      // let isAndroid = Math.random() > 0.5
+      let isAndroid = false
+      if (account.duid) {
+        isAndroid = !account.duid.includes('-')
+      }
+      if (isAndroid && !account.deviceModel) {
+        account.deviceModel = getAndroidModel()
+      }
+      if (!account.duid) {
+        account.duid = isAndroid
+          ? await getDeviceId(isAndroid, 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': '45',
+              'lunch-fiat-currency': 'USD',
+              'lunch-app-duid': account.duid,
+              'user-agent': `Lunch/1.0 (xyz.lunchlunch.app; build:45; ${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}`
+      // await client.delete('http://api.lunchlunch.xyz/v1/member/delete')
+      // return
+      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 polly()
+        .waitAndRetry(10)
+        .executeForPromise(async info => {
+          if (info.count) console.log(`${index}: retry ${info.count}`)
+          try {
+            await client.post(
+              'https://api.lunchlunch.xyz/v1/dish/submit-action/airdrop',
+              {
+                dishId: 43,
+              },
+            )
+            console.log(`${index}: airdrop done`)
+          } catch (e) {
+            console.log(e.response.status, e.response?.data)
+            throw e
+          }
+        })
+
+      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,
+        },
+      })
+    } catch (e) {
+      console.log(e)
+      await DBClient.instance.account.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,
+        },
+      })
+    }
+  })
+}
+
+
+// await faucetLunch(1)
+//
+// 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()

+ 0 - 1
src/test.ts

@@ -1,4 +1,3 @@
-import { faucetAccount } from './faucet/faucet'
 
 import { AccAddress, MnemonicKey, RawKey } from '@initia/initia.js'
 import { InitiaClient } from './InitiaClient'

+ 2 - 2
src/utils/index.ts

@@ -62,8 +62,8 @@ export function getAxiosClient(useProxy: boolean) {
   })
 }
 
-export async function forEachAsync(
-  array: any[],
+export async function forEachAsync<T = any>(
+  array: T[],
   concurrency: number,
   asyncFn: (item: any, index: number) => Promise<void>,
   timeout: number = 320000,

+ 11 - 1
yarn.lock

@@ -1427,6 +1427,11 @@
   resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
   integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
 
+"@types/node-cron@^3.0.11":
+  version "3.0.11"
+  resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344"
+  integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==
+
 "@types/node@*", "@types/node@>=13.7.0", "@types/node@~18":
   version "18.19.33"
   resolved "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz"
@@ -1454,6 +1459,11 @@
   resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz"
   integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
 
+"@types/uuid@^9.0.8":
+  version "9.0.8"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
+  integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
+
 "@types/yargs-parser@*":
   version "21.0.3"
   resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz"
@@ -3542,7 +3552,7 @@ node-addon-api@^5.0.0:
 
 node-cron@^3.0.3:
   version "3.0.3"
-  resolved "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.3.tgz#c4bc7173dd96d96c50bdb51122c64415458caff2"
   integrity sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==
   dependencies:
     uuid "8.3.2"