diff --git a/mainnetCheckService.ts b/mainnetCheckService.ts new file mode 100644 index 0000000..c823cc3 --- /dev/null +++ b/mainnetCheckService.ts @@ -0,0 +1,128 @@ +import { DynamoDBClient } from "@aws-sdk/client-dynamodb" +import { DynamoDBDocumentClient, GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb" +import { getNonce } from "./utils/mainnetBalanceCheck"; + +const MAINNET_CHECK_TRACKER_TABLE = "mainnet_check_tracker"; +const MAX_ADDRESS_CHECK_COUNT = 3; + +type AddressStatus = { + checkCount: number, + lastUsedNonce: number, +} + +export class MainnetCheckService { + private readonly documentClient: DynamoDBDocumentClient + private readonly RPC: string + + constructor(rpc: string) { + const ddbClient = new DynamoDBClient({ region: 'us-east-1' }) + this.documentClient = DynamoDBDocumentClient.from(ddbClient) + this.RPC = rpc + } + + /** + * 1. Get check count and last used nonce (address status) + * 2. If checkCount < MAX_ADDRESS_CHECK_COUNT: + * a. Asynchronously increment check count and update nonce + * b. Return success + * 3. If checkCount == MAX_ADDRESS_CHECK_COUNT + * a. Fetch current nonce from network, and last used nonce from DB + * b. diff = currentNonce - lastUsedNonce + * c. If diff > 0 + * i. checkCount = max(0, checkCount - diff) + * ii. asynchronously update address status + * iii. return success + * d. If diff == 0 + * i. fail + */ + async checkAddressValidity(address: string): Promise { + let addressStatus = await this.getAddressStatus(address) + if (!addressStatus) { + // update address status + addressStatus = await this.updateAddressStatus(address) + } + + if (addressStatus.checkCount < MAX_ADDRESS_CHECK_COUNT) { + this.updateAddressStatus(address, ++addressStatus.checkCount, addressStatus.lastUsedNonce) + return true + } else { + const currentNonce = await getNonce(this.RPC, address) + if (!currentNonce) { + throw "Error fetching nonce..." + } + const diff = currentNonce - addressStatus.lastUsedNonce + if (diff > 0) { + const updatedCheckCount = Math.max(0, addressStatus.checkCount - diff) + 1 + this.updateAddressStatus(address, updatedCheckCount, currentNonce) + return true + } else { + return false + } + } + } + + // Utility + + async getAddressStatus(address: string): Promise { + const params = { + TableName: MAINNET_CHECK_TRACKER_TABLE, + Key: { address } + }; + const command = new GetCommand(params); + + try { + const data = await this.documentClient.send(command); + if (!data.Item) { + return undefined; + } + + return { + checkCount: data.Item.checkCount, + lastUsedNonce: data.Item.lastUsedNonce, + } + } catch (error) { + console.error(JSON.stringify({ + date: new Date(), + type: 'GetAddressStatusError', + item: error + })); + } + } + + async updateAddressStatus(address: string, checkCount: number = 0, nonce?: number): Promise { + // if nonce is not provided, fetch from network + if (!nonce) { + const currentNonce = await getNonce(this.RPC, address) + if (!currentNonce) { + throw "Error fetching nonce..." + } + nonce = currentNonce + } + + const params = { + TableName: MAINNET_CHECK_TRACKER_TABLE, + Item: { + address, + lastUsedNonce: nonce, + checkCount, + } + }; + + const command = new PutCommand(params); + + try { + await this.documentClient.send(command); + } catch (error) { + console.error(JSON.stringify({ + date: new Date(), + type: 'PuttingAddressTrackerError', + item: error + })); + } + + return { + checkCount, + lastUsedNonce: nonce, + } + } +} \ No newline at end of file diff --git a/server.ts b/server.ts index 5749ced..a5dd5f0 100644 --- a/server.ts +++ b/server.ts @@ -30,6 +30,7 @@ import { checkMainnetBalancePipeline, pipelineFailureMessage } from './utils/pipelineChecks' +import { MainnetCheckService } from './mainnetCheckService' dotenv.config() @@ -64,6 +65,7 @@ new RateLimiter(app, [ }) const couponService = new CouponService(couponConfig) +const mainnetCheckService = new MainnetCheckService(MAINNET_BALANCE_CHECK_RPC) const captcha: VerifyCaptcha = new VerifyCaptcha(app, process.env.CAPTCHA_SECRET!, process.env.V2_CAPTCHA_SECRET!) @@ -152,7 +154,7 @@ router.post('/sendToken', captcha.middleware, async (req: any, res: any) => { !pipelineValidity.isValid && couponCheckEnabled && await checkCouponPipeline(couponService, pipelineValidity, faucetConfigId, coupon) // don't check mainnet balance, if coupon is provided - !pipelineValidity.isValid && !coupon && mainnetCheckEnabled && await checkMainnetBalancePipeline(pipelineValidity, MAINNET_BALANCE_CHECK_RPC, address) + !pipelineValidity.isValid && !coupon && mainnetCheckEnabled && await checkMainnetBalancePipeline(mainnetCheckService, pipelineValidity, MAINNET_BALANCE_CHECK_RPC, address) if ( (mainnetCheckEnabled || couponCheckEnabled) && diff --git a/utils/mainnetBalanceCheck.ts b/utils/mainnetBalanceCheck.ts index 7254c87..d6f2e76 100644 --- a/utils/mainnetBalanceCheck.ts +++ b/utils/mainnetBalanceCheck.ts @@ -23,3 +23,22 @@ export async function checkMainnetBalance(rpc: string, address: string, threshol } return response } + +export async function getNonce(rpc: string, address: string): Promise { + try { + const response = await axios.post(rpc, { + jsonrpc: "2.0", + method: "eth_getTransactionCount", + params: [address, "latest"], + id: 1, + }) + return parseInt(response.data.result) + } catch(err) { + console.error(JSON.stringify({ + date: new Date(), + type: 'NonceCheckError', + item: err + })) + return undefined + } +} diff --git a/utils/pipelineChecks.ts b/utils/pipelineChecks.ts index f047f77..4568a5c 100644 --- a/utils/pipelineChecks.ts +++ b/utils/pipelineChecks.ts @@ -1,4 +1,5 @@ import { CouponService } from '../CouponService/couponService' +import { MainnetCheckService } from '../mainnetCheckService' import { checkMainnetBalance } from './mainnetBalanceCheck' export enum PIPELINE_CHECKS { @@ -48,12 +49,21 @@ export async function checkCouponPipeline( } } -export async function checkMainnetBalancePipeline(pipelineCheckValidity: PipelineCheckValidity, rpc: string, address: string) { +export async function checkMainnetBalancePipeline( + mainnetCheckService: MainnetCheckService, + pipelineCheckValidity: PipelineCheckValidity, + rpc: string, + address: string, +) { const {isValid, balance} = await checkMainnetBalance(rpc, address) if (isValid) { - pipelineCheckValidity.isValid = true - pipelineCheckValidity.checkPassedType = PIPELINE_CHECKS.MAINNET_BALANCE - pipelineCheckValidity.mainnetBalance = balance + if (await mainnetCheckService.checkAddressValidity(address)) { + pipelineCheckValidity.isValid = true + pipelineCheckValidity.checkPassedType = PIPELINE_CHECKS.MAINNET_BALANCE + pipelineCheckValidity.mainnetBalance = balance + } else { + pipelineCheckValidity.errorMessage = "Address has exhausted maximum balance checks! Please do some mainnet transactinos." + } } else { pipelineCheckValidity.errorMessage = "Mainnet balance check failed! " }