diff --git a/package-lock.json b/package-lock.json index 92531ae..ce8ce13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "auctioneer-bot", - "version": "0.2.2", + "version": "2.0.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "auctioneer-bot", - "version": "0.2.2", + "version": "2.0.0-beta", "license": "MIT", "dependencies": { - "@blend-capital/blend-sdk": "3.0.0-beta.6", - "@stellar/stellar-sdk": "13.1.0", + "@blend-capital/blend-sdk": "3.0.0", + "@stellar/stellar-sdk": "13.2.0", "better-sqlite3": "^11.1.2", "winston": "^3.13.1", "winston-daily-rotate-file": "^5.0.0" @@ -462,11 +462,11 @@ "dev": true }, "node_modules/@blend-capital/blend-sdk": { - "version": "3.0.0-beta.6", - "resolved": "https://registry.npmjs.org/@blend-capital/blend-sdk/-/blend-sdk-3.0.0-beta.6.tgz", - "integrity": "sha512-UnSSI0wQjUXOuzgKeVU2MwwIKF3zGhxmqqGX1nkFQ/KlF6emviiyGI/BwrWJGwIVKOrqoGk+ZZ/Ycu5NVnus7w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@blend-capital/blend-sdk/-/blend-sdk-3.0.0.tgz", + "integrity": "sha512-oJELTOOr1YyNaXRtgbPPI/TlbAhTkf2PzJLpZIJbfvxW0gixgDqtv7OG3BjXrlyVAEbnQUGt3GFZX1XHljWVhQ==", "dependencies": { - "@stellar/stellar-sdk": "13.1.0", + "@stellar/stellar-sdk": "13.2.0", "buffer": "6.0.3", "follow-redirects": ">=1.15.6" } @@ -871,10 +871,9 @@ "license": "Apache-2.0" }, "node_modules/@stellar/stellar-base": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.0.1.tgz", - "integrity": "sha512-Xbd12mc9Oj/130Tv0URmm3wXG77XMshZtZ2yNCjqX5ZbMD5IYpbBs3DVCteLU/4SLj/Fnmhh1dzhrQXnk4r+pQ==", - "license": "Apache-2.0", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", + "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", "dependencies": { "@stellar/js-xdr": "^3.1.2", "base32.js": "^0.1.0", @@ -883,23 +882,29 @@ "sha.js": "^2.3.6", "tweetnacl": "^1.0.3" }, + "engines": { + "node": ">=18.0.0" + }, "optionalDependencies": { - "sodium-native": "^4.3.0" + "sodium-native": "^4.3.3" } }, "node_modules/@stellar/stellar-sdk": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.1.0.tgz", - "integrity": "sha512-ARQkUdyGefXdTgwSF0eONkzv/geAqUfyfheJ9Nthz6GAr5b41fNwWW9UtE8JpXC4IpvE3t5elIUN5hKJzASN9w==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.2.0.tgz", + "integrity": "sha512-azxeh1+mS28h96Q+vl41ffytQvWdudRl1KtjYO0TRZb/9u0/lH57oYBeJ+gvcQr+T7s02wTayFdHbKru5V5/XA==", "dependencies": { - "@stellar/stellar-base": "^13.0.1", - "axios": "^1.7.9", + "@stellar/stellar-base": "^13.1.0", + "axios": "^1.8.4", "bignumber.js": "^9.1.2", "eventsource": "^2.0.2", "feaxios": "^0.0.23", "randombytes": "^2.1.0", "toml": "^3.0.0", "urijs": "^1.19.1" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@types/amqplib": { @@ -1234,6 +1239,74 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-addon-resolve": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.4.tgz", + "integrity": "sha512-unn6Vy/Yke6F99vg/7tcrvM2KUvIhTNniaSqDbam4AWkd4NhvDVSrQiRYVlNzUV2P7SPobkCK7JFVxrJk9btCg==", + "optional": true, + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-module-resolve": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.10.2.tgz", + "integrity": "sha512-C9COe/GhWfVXKytW3DElTkiBU+Gb2OXeaVkdGdRB/lp26TVLESHkTGS876iceAGdvtPgohfp9nX8vXHGvN3++Q==", + "optional": true, + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-semver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.1.tgz", + "integrity": "sha512-UtggzHLiTrmFOC/ogQ+Hy7VfoKoIwrP1UFcYtTxoCUdLtsIErT8+SWtOC2DH/snT9h+xDrcBEPcwKei1mzemgg==", + "optional": true + }, + "node_modules/bare-url": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.1.5.tgz", + "integrity": "sha512-lNImB5KLN+ggw+SYDYvqf/yCizXIyq8U/nWBlx7m4pc4TKS24SB/1WWskzGacon5cVVAC6qUzCYzI/aMYCf4Ng==", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base32.js": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", @@ -3304,18 +3377,6 @@ "node": ">=10" } }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3689,6 +3750,19 @@ "node": ">= 6" } }, + "node_modules/require-addon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.1.0.tgz", + "integrity": "sha512-KbXAD5q2+v1GJnkzd8zzbOxchTkStSyJZ9QwoCq3QwEXAaIlG3wDYRZGzVD357jmwaGY7hr5VaoEAL0BkF0Kvg==", + "optional": true, + "dependencies": { + "bare-addon-resolve": "^1.3.0", + "bare-url": "^2.1.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3893,13 +3967,12 @@ } }, "node_modules/sodium-native": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.1.tgz", - "integrity": "sha512-YdP64gAdpIKHfL4ttuX4aIfjeunh9f+hNeQJpE9C8UMndB3zkgZ7YmmGT4J2+v6Ibyp6Wem8D1TcSrtdW0bqtg==", - "license": "MIT", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", + "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", "optional": true, "dependencies": { - "node-gyp-build": "^4.8.0" + "require-addon": "^1.1.0" } }, "node_modules/source-map": { diff --git a/package.json b/package.json index d5df196..5071f6c 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "auctioneer-bot", - "version": "1.0.0-beta", + "version": "2.0.0-beta", "main": "index.js", "type": "module", "scripts": { "build": "tsc", - "build:docker-arm": "npm run build && docker buildx build --platform=linux/arm64 -t auctioneer-bot-arm .", - "build:docker-x86": "npm run build && docker buildx build --platform=linux/amd64 -t auctioneer-bot-x86 .", + "build:docker-arm": "npm run build && docker buildx build --platform=linux/arm64 -t auctioneer-bot-v2-arm .", + "build:docker-x86": "npm run build && docker buildx build --platform=linux/amd64 -t auctioneer-bot-v2-x86 .", "test": "jest --config jest.config.cjs" }, "author": "gm@script3.io", @@ -25,8 +25,8 @@ "typescript": "^5.5.4" }, "dependencies": { - "@blend-capital/blend-sdk": "3.0.0-beta.6", - "@stellar/stellar-sdk": "13.1.0", + "@blend-capital/blend-sdk": "3.0.0", + "@stellar/stellar-sdk": "13.2.0", "better-sqlite3": "^11.1.2", "winston": "^3.13.1", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/bidder_handler.ts b/src/bidder_handler.ts index 8192261..166ec34 100644 --- a/src/bidder_handler.ts +++ b/src/bidder_handler.ts @@ -1,5 +1,10 @@ import { calculateAuctionFill } from './auction.js'; -import { AuctionBid, BidderSubmissionType, BidderSubmitter } from './bidder_submitter.js'; +import { + AddAllowance, + AuctionBid, + BidderSubmissionType, + BidderSubmitter, +} from './bidder_submitter.js'; import { AppEvent, EventType } from './events.js'; import { APP_CONFIG } from './utils/config.js'; import { AuctioneerDatabase, AuctionType } from './utils/db.js'; @@ -50,6 +55,21 @@ export class BidderHandler { const ledgersToFill = auctionEntry.fill_block - nextLedger; if (auctionEntry.fill_block === 0 || ledgersToFill <= 5 || ledgersToFill % 10 === 0) { + // Check if the filler has an active allowance for backstop token + if ( + auctionEntry.auction_type === AuctionType.Interest && + auctionEntry.fill_block === 0 + ) { + let allowanceCheck: AddAllowance = { + type: BidderSubmissionType.ADD_ALLOWANCE, + filler: filler, + assetId: APP_CONFIG.backstopTokenAddress, + spender: APP_CONFIG.backstopAddress, + currLedger: appEvent.ledger, + }; + this.submissionQueue.addSubmission(allowanceCheck, 4); + } + // recalculate the auction const auction = await this.sorobanHelper.loadAuction( auctionEntry.pool_id, diff --git a/src/bidder_submitter.ts b/src/bidder_submitter.ts index b028474..0e7f116 100644 --- a/src/bidder_submitter.ts +++ b/src/bidder_submitter.ts @@ -1,8 +1,8 @@ -import { PoolContractV1 } from '@blend-capital/blend-sdk'; -import { rpc } from '@stellar/stellar-sdk'; +import { ContractErrorType, PoolContractV2 } from '@blend-capital/blend-sdk'; +import { Address, Contract, nativeToScVal, rpc } from '@stellar/stellar-sdk'; import { calculateAuctionFill } from './auction.js'; import { getFillerAvailableBalances, managePositions } from './filler.js'; -import { Filler } from './utils/config.js'; +import { APP_CONFIG, Filler } from './utils/config.js'; import { AuctioneerDatabase, AuctionEntry, AuctionType } from './utils/db.js'; import { serializeError, stringify } from './utils/json.js'; import { logger } from './utils/logger.js'; @@ -10,11 +10,12 @@ import { sendSlackNotification } from './utils/slack_notifier.js'; import { SorobanHelper } from './utils/soroban_helper.js'; import { SubmissionQueue } from './utils/submission_queue.js'; -export type BidderSubmission = AuctionBid | FillerUnwind; +export type BidderSubmission = AuctionBid | FillerUnwind | AddAllowance; export enum BidderSubmissionType { BID = 'bid', UNWIND = 'unwind', + ADD_ALLOWANCE = 'add_allowance', } export interface BaseBidderSubmission { @@ -33,6 +34,17 @@ export interface FillerUnwind extends BaseBidderSubmission { filler: Filler; } +/** + * Event to check for allowance updates. + */ +export interface AddAllowance extends BaseBidderSubmission { + type: BidderSubmissionType.ADD_ALLOWANCE; + filler: Filler; + assetId: string; + spender: string; + currLedger: number; +} + export class BidderSubmitter extends SubmissionQueue { db: AuctioneerDatabase; @@ -66,6 +78,8 @@ export class BidderSubmitter extends SubmissionQueue { return this.submitBid(sorobanHelper, submission); case BidderSubmissionType.UNWIND: return this.submitUnwind(sorobanHelper, submission); + case BidderSubmissionType.ADD_ALLOWANCE: + return this.submitAddAllowance(sorobanHelper, submission); default: logger.error(`Invalid submission type: ${stringify(submission)}`); // consume the submission @@ -105,7 +119,7 @@ export class BidderSubmitter extends SubmissionQueue { ); if (nextLedger >= fill.block) { - const pool = new PoolContractV1(auctionBid.auctionEntry.pool_id); + const pool = new PoolContractV2(auctionBid.auctionEntry.pool_id); const result = await sorobanHelper.submitTransaction( pool.submit({ @@ -225,7 +239,7 @@ export class BidderSubmitter extends SubmissionQueue { if (requests.length > 0) { logger.info('Unwind found positions to manage', requests); // some positions to manage - submit the transaction - const pool = new PoolContractV1(fillerUnwind.poolId); + const pool = new PoolContractV2(fillerUnwind.poolId); const result = await sorobanHelper.submitTransaction( pool.submit({ from: filler_pubkey, @@ -267,6 +281,47 @@ export class BidderSubmitter extends SubmissionQueue { return true; } + async submitAddAllowance( + sorobanHelper: SorobanHelper, + allowance: AddAllowance + ): Promise { + try { + const allowanceData = await sorobanHelper.loadAllowance( + allowance.assetId, + allowance.filler.keypair.publicKey(), + allowance.spender + ); + if ( + allowanceData.amount < BigInt(100_000e7) || + allowanceData.expiration_ledger < allowance.currLedger + 17368 * 7 + ) { + const assetContract = new Contract(allowance.assetId); + const op = assetContract + .call( + 'approve', + ...[ + Address.fromString(allowance.filler.keypair.publicKey()).toScVal(), + Address.fromString(allowance.spender).toScVal(), + nativeToScVal(BigInt('18446744073709551615'), { type: 'i128' }), + nativeToScVal(allowance.currLedger + 17368 * 30 * 5, { type: 'u32' }), + ] + ) + .toXDR('base64'); + await sorobanHelper.submitTransaction(op, allowance.filler.keypair); + + const logMessage = + `Successfully updated allowance\n` + + `Filler: ${allowance.filler.name}\n` + + `Spender: ${allowance.spender}\n` + + `Asset: ${allowance.assetId}\n`; + logger.info(logMessage); + return true; // TODO: Check for error in response + } + return true; + } catch (e) { + return false; + } + } async onDrop(submission: BidderSubmission): Promise { let logMessage: string; switch (submission.type) { @@ -286,6 +341,14 @@ export class BidderSubmitter extends SubmissionQueue { `Filler: ${submission.filler.name}\n` + `Pool: ${submission.poolId}`; break; + case BidderSubmissionType.ADD_ALLOWANCE: + logMessage = + `Dropped allowance check\n` + + `Filler: ${submission.filler.name}\n` + + `Spender: ${submission.spender}\n` + + `Asset: ${submission.assetId}\n` + + `Ledger: ${submission.currLedger}`; + break; } logger.error(logMessage); await sendSlackNotification(logMessage); diff --git a/src/collector.ts b/src/collector.ts index d046236..5c84e07 100644 --- a/src/collector.ts +++ b/src/collector.ts @@ -1,4 +1,4 @@ -import { poolEventFromEventResponse } from '@blend-capital/blend-sdk'; +import { poolEventV2FromEventResponse } from '@blend-capital/blend-sdk'; import { rpc } from '@stellar/stellar-sdk'; import { ChildProcess } from 'child_process'; import { @@ -125,7 +125,7 @@ export async function runCollector( let cursor = ''; while (events.events.length > 0) { for (const raw_event of events.events) { - let blendPoolEvent = poolEventFromEventResponse(raw_event); + let blendPoolEvent = poolEventV2FromEventResponse(raw_event); if (blendPoolEvent) { // handle pool events immediately let poolEvent: PoolEventEvent = { diff --git a/src/events.ts b/src/events.ts index dd5a5b7..eaa0a2e 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,4 +1,4 @@ -import { PoolEvent } from '@blend-capital/blend-sdk'; +import { PoolV2Event } from '@blend-capital/blend-sdk'; export enum EventType { VALIDATE_POOLS = 'validate_pools', LEDGER = 'ledger', @@ -43,7 +43,7 @@ export interface LedgerEvent extends BaseEvent { */ export interface PoolEventEvent extends BaseEvent { type: EventType.POOL_EVENT; - event: PoolEvent; + event: PoolV2Event; } // ********** Work Queue Only ********** diff --git a/src/liquidations.ts b/src/liquidations.ts index bfa09dc..42d43c7 100644 --- a/src/liquidations.ts +++ b/src/liquidations.ts @@ -1,4 +1,4 @@ -import { PositionsEstimate } from '@blend-capital/blend-sdk'; +import { Pool, PoolOracle, Positions, PositionsEstimate } from '@blend-capital/blend-sdk'; import { updateUser } from './user.js'; import { APP_CONFIG } from './utils/config.js'; import { AuctioneerDatabase, AuctionType } from './utils/db.js'; @@ -40,15 +40,17 @@ export function isBadDebt(user: PositionsEstimate): boolean { * @param user - The positions estimate of the user * @returns The liquidation percent */ -export function calculateLiquidationPercent(user: PositionsEstimate): bigint { +export function calculateLiquidationPercent(user: PositionsEstimate): number { const avgInverseLF = user.totalEffectiveLiabilities / user.totalBorrowed; const avgCF = user.totalEffectiveCollateral / user.totalSupplied; const estIncentive = 1 + (1 - avgCF / avgInverseLF) / 2; const numerator = user.totalEffectiveLiabilities * 1.06 - user.totalEffectiveCollateral; const denominator = avgInverseLF * 1.06 - avgCF * estIncentive; - const liqPercent = BigInt( - Math.min(Math.round((numerator / denominator / user.totalBorrowed) * 100), 100) + const liqPercent = Math.min( + Math.round((numerator / denominator / user.totalBorrowed) * 100), + 100 ); + logger.info( `Calculated liquidation percent ${liqPercent} with est incentive ${estIncentive} numerator ${numerator} and denominator ${denominator} for user ${user}.` ); @@ -112,8 +114,15 @@ export async function checkUsersForLiquidationsAndBadDebt( (await sorobanHelper.loadAuction(poolId, user, AuctionType.BadDebt)) === undefined ) { submissions.push({ - type: WorkSubmissionType.BadDebtAuction, + type: WorkSubmissionType.AuctionCreation, poolId, + user: APP_CONFIG.backstopAddress, + auctionType: AuctionType.BadDebt, + auctionPercent: 100, + bid: Array.from(backstop.positions.liabilities.keys()).map( + (index) => pool.metadata.reserveList[index] + ), + lot: [APP_CONFIG.backstopTokenAddress], }); } } else if ( @@ -123,12 +132,19 @@ export async function checkUsersForLiquidationsAndBadDebt( await sorobanHelper.loadUserPositionEstimate(poolId, user); updateUser(db, pool, poolUser, poolUserEstimate); if (isLiquidatable(poolUserEstimate)) { - const liquidationPercent = calculateLiquidationPercent(poolUserEstimate); + const auctionPercent = calculateLiquidationPercent(poolUserEstimate); submissions.push({ - type: WorkSubmissionType.LiquidateUser, + type: WorkSubmissionType.AuctionCreation, poolId, - user: user, - liquidationPercent: liquidationPercent, + user, + auctionPercent, + auctionType: AuctionType.Liquidation, + bid: Array.from(poolUser.positions.liabilities.keys()).map( + (index) => pool.metadata.reserveList[index] + ), + lot: Array.from(poolUser.positions.collateral.keys()).map( + (index) => pool.metadata.reserveList[index] + ), }); } else if (isBadDebt(poolUserEstimate)) { submissions.push({ diff --git a/src/pool_event_handler.ts b/src/pool_event_handler.ts index 4293a79..5324837 100644 --- a/src/pool_event_handler.ts +++ b/src/pool_event_handler.ts @@ -1,4 +1,4 @@ -import { PoolEventType, PoolFillAuctionV1Event } from '@blend-capital/blend-sdk'; +import { PoolEventType } from '@blend-capital/blend-sdk'; import { ChildProcess } from 'child_process'; import { EventType, PoolEventEvent } from './events.js'; import { canFillerBid } from './filler.js'; @@ -78,6 +78,7 @@ export class PoolEventHandler { case PoolEventType.SupplyCollateral: case PoolEventType.WithdrawCollateral: case PoolEventType.Borrow: + case PoolEventType.FlashLoan: case PoolEventType.Repay: { // update the user in the db const { estimate: userPositionsEstimate, user } = @@ -85,24 +86,8 @@ export class PoolEventHandler { updateUser(this.db, pool, user, userPositionsEstimate, poolEvent.event.ledger); break; } - case PoolEventType.NewLiquidationAuction: - case PoolEventType.NewAuction: { - let auction_type: AuctionType; - if ('auctionType' in poolEvent.event) { - auction_type = poolEvent.event.auctionType; - } else { - // New liquidation auction events do not have an auction type - auction_type = AuctionType.Liquidation; - } - - let user: string; - // V1 interest auctions and bad debt auctions have no user - if ('user' in poolEvent.event) { - user = poolEvent.event.user; - } else { - user = APP_CONFIG.backstopAddress; - } + case PoolEventType.NewAuction: { // check if the auction should be bid on by an auctioneer let fillerFound = false; for (const filler of APP_CONFIG.fillers) { @@ -112,8 +97,8 @@ export class PoolEventHandler { } let auctionEntry: AuctionEntry = { pool_id: poolId, - user_id: user, - auction_type: auction_type, + user_id: poolEvent.event.user, + auction_type: poolEvent.event.auctionType, filler: filler.keypair.publicKey(), start_block: poolEvent.event.auctionData.block, fill_block: 0, @@ -123,10 +108,10 @@ export class PoolEventHandler { const logMessage = `New auction\n` + - `Type: ${AuctionType[auction_type]}\n` + + `Type: ${AuctionType[poolEvent.event.auctionType]}\n` + `Filler: ${filler.name}\n` + `Pool: ${poolId}\n` + - `User: ${user}\n` + + `User: ${poolEvent.event.user}\n` + `Auction Data: ${stringify(poolEvent.event.auctionData, 2)}\n`; await sendSlackNotification(logMessage); logger.info(logMessage); @@ -136,9 +121,9 @@ export class PoolEventHandler { if (!fillerFound) { const logMessage = `Auction Ignored\n` + - `Type: ${AuctionType[auction_type]}\n` + + `Type: ${AuctionType[poolEvent.event.auctionType]}\n` + `Pool: ${poolId}\n` + - `User: ${user}\n` + + `User: ${poolEvent.event.user}\n` + `Auction Data: ${stringify(poolEvent.event.auctionData, 2)}\n`; await sendSlackNotification(logMessage); logger.info(logMessage); @@ -163,7 +148,7 @@ export class PoolEventHandler { break; } case PoolEventType.FillAuction: { - const fillerAddress = (poolEvent.event as PoolFillAuctionV1Event).from; + const fillerAddress = poolEvent.event.filler; const logMessage = `Auction Fill Event\n` + `Type ${AuctionType[poolEvent.event.auctionType]}\n` + @@ -224,6 +209,20 @@ export class PoolEventHandler { }); break; } + case PoolEventType.DeleteAuction: { + const user = poolEvent.event.user; + const auctionType = poolEvent.event.auctionType; + let runResult = this.db.deleteAuctionEntry(poolId, user, auctionType); + if (runResult.changes !== 0) { + const logMessage = + `Stale Auction Deleted\n` + + `Type: ${AuctionType[auctionType]}\n` + + `Pool: ${poolId}\n` + + `User: ${user}`; + await sendSlackNotification(logMessage); + logger.info(logMessage); + } + } default: { logger.error(`Unhandled event type: ${poolEvent.event.eventType}`); break; diff --git a/src/utils/soroban_helper.ts b/src/utils/soroban_helper.ts index 7cdfd14..609cdad 100644 --- a/src/utils/soroban_helper.ts +++ b/src/utils/soroban_helper.ts @@ -9,11 +9,12 @@ import { PoolContract, PoolOracle, PoolUser, - PoolV1, + PoolV2, PositionsEstimate, } from '@blend-capital/blend-sdk'; import { Account, + Address, BASE_FEE, Contract, Keypair, @@ -75,7 +76,7 @@ export class SorobanHelper { if (cachedPool) { return cachedPool; } - let pool: Pool = await PoolV1.load(this.network, poolId); + let pool: Pool = await PoolV2.load(this.network, poolId); this.pool_cache.set(poolId, pool); return pool; } catch (e: any) { @@ -195,6 +196,63 @@ export class SorobanHelper { } } + async loadAllowance( + tokenId: string, + from: string, + spender: string + ): Promise<{ expiration_ledger: number; amount: bigint }> { + try { + const stellarRpc = new rpc.Server(this.network.rpc, this.network.opts); + const res: xdr.ScVal[] = [ + xdr.ScVal.scvSymbol('Allowance'), + xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('from'), + val: Address.fromString(from).toScVal(), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('spender'), + val: Address.fromString(spender).toScVal(), + }), + ]), + ]; + const ledgerKey = xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: Address.fromString(tokenId).toScAddress(), + key: xdr.ScVal.scvVec(res), + durability: xdr.ContractDataDurability.temporary(), + }) + ); + const ledgerData = await stellarRpc.getLedgerEntries(ledgerKey); + + if (ledgerData.entries.length !== 1) { + return { + expiration_ledger: 0, + amount: 0n, + }; + } + let allowance = scValToNative(ledgerData.entries[0].val.contractData().val()); + + if ( + allowance === undefined || + allowance.expiration_ledger === undefined || + allowance.amount === undefined + ) { + throw new Error('Invalid allowance data'); + } + + return allowance; + } catch (e) { + logger.error( + `Error loading allowance expiration for tokenId: ${tokenId}\n` + + `from: ${from}\n` + + `spender: ${spender}\n` + + `Error: ${e}` + ); + throw e; + } + } + async simLPTokenToUSDC(backstopAddress: string, amount: bigint): Promise { try { let comet = new Contract(APP_CONFIG.backstopTokenAddress); diff --git a/src/work_submitter.ts b/src/work_submitter.ts index 3827142..4210b54 100644 --- a/src/work_submitter.ts +++ b/src/work_submitter.ts @@ -1,18 +1,18 @@ -import { ContractError, ContractErrorType, PoolContractV1 } from '@blend-capital/blend-sdk'; -import { APP_CONFIG } from './utils/config.js'; +import { ContractError, ContractErrorType, PoolContractV2 } from '@blend-capital/blend-sdk'; +import { APP_CONFIG, Filler } from './utils/config.js'; import { AuctionType } from './utils/db.js'; import { serializeError, stringify } from './utils/json.js'; import { logger } from './utils/logger.js'; import { sendSlackNotification } from './utils/slack_notifier.js'; import { SorobanHelper } from './utils/soroban_helper.js'; import { SubmissionQueue } from './utils/submission_queue.js'; +import { Address, Contract, nativeToScVal } from '@stellar/stellar-sdk'; -export type WorkSubmission = UserLiquidation | BadDebtTransfer | BadDebtAuction; +export type WorkSubmission = AuctionCreation | BadDebtTransfer; export enum WorkSubmissionType { - LiquidateUser = 'liquidate', + AuctionCreation = 'create_auction', BadDebtTransfer = 'bad_debt_transfer', - BadDebtAuction = 'bad_debt_auction', } export interface BaseWorkSubmission { @@ -25,14 +25,13 @@ export interface BadDebtTransfer extends BaseWorkSubmission { user: string; } -export interface UserLiquidation extends BaseWorkSubmission { - type: WorkSubmissionType.LiquidateUser; +export interface AuctionCreation extends BaseWorkSubmission { + type: WorkSubmissionType.AuctionCreation; user: string; - liquidationPercent: bigint; -} - -export interface BadDebtAuction extends BaseWorkSubmission { - type: WorkSubmissionType.BadDebtAuction; + auctionType: AuctionType; + auctionPercent: number; + bid: string[]; + lot: string[]; } export class WorkSubmitter extends SubmissionQueue { @@ -45,12 +44,10 @@ export class WorkSubmitter extends SubmissionQueue { let sorobanHelper = new SorobanHelper(); switch (submission.type) { - case WorkSubmissionType.LiquidateUser: - return this.submitUserLiquidation(sorobanHelper, submission); + case WorkSubmissionType.AuctionCreation: + return this.submitAuction(sorobanHelper, submission); case WorkSubmissionType.BadDebtTransfer: return this.submitBadDebtTransfer(sorobanHelper, submission); - case WorkSubmissionType.BadDebtAuction: - return this.submitBadDebtAuction(sorobanHelper, submission); default: logger.error(`Invalid submission type: ${stringify(submission)}`); // consume the submission @@ -58,39 +55,38 @@ export class WorkSubmitter extends SubmissionQueue { } } - async submitUserLiquidation( - sorobanHelper: SorobanHelper, - userLiquidation: UserLiquidation - ): Promise { + async submitAuction(sorobanHelper: SorobanHelper, auction: AuctionCreation): Promise { try { - logger.info( - `Creating liquidation for user: ${userLiquidation.user} in pool: ${userLiquidation.poolId}` - ); - - const pool = new PoolContractV1(userLiquidation.poolId); - const op = pool.newLiquidationAuction({ - user: userLiquidation.user, - percent_liquidated: userLiquidation.liquidationPercent, + logger.info(`Creating liquidation for user: ${auction.user} in pool: ${auction.poolId}`); + + const pool = new PoolContractV2(auction.poolId); + const op = pool.newAuction({ + user: auction.user, + auction_type: auction.auctionType, + percent: auction.auctionPercent, + bid: auction.bid, + lot: auction.lot, }); const auctionExists = - (await sorobanHelper.loadAuction( - userLiquidation.poolId, - userLiquidation.user, - AuctionType.Liquidation - )) !== undefined; + (await sorobanHelper.loadAuction(auction.poolId, auction.user, AuctionType.Liquidation)) !== + undefined; if (auctionExists) { logger.info( - `User liquidation auction already exists for user: ${userLiquidation.user} in pool: ${userLiquidation.poolId}` + `User liquidation auction already exists for user: ${auction.user} in pool: ${auction.poolId}` ); return true; } await sorobanHelper.submitTransaction(op, APP_CONFIG.keypair); const logMessage = - `Successfully created user liquidation\n` + - `Pool: ${userLiquidation.poolId}\n` + - `User: ${userLiquidation.user}\n` + - `Liquidation Percent: ${userLiquidation.liquidationPercent}`; + `Successfully created auction\n` + + `Auction Type: ${AuctionType[auction.auctionType]}\n` + + `Pool: ${auction.poolId}\n` + + `User: ${auction.user}\n` + + `Auction Percent: ${auction.auctionPercent}\n` + + `Bid: ${stringify(auction.bid)}\n` + + `Lot: ${stringify(auction.lot)}\n`; + logger.info(logMessage); await sendSlackNotification(logMessage); return true; @@ -98,23 +94,21 @@ export class WorkSubmitter extends SubmissionQueue { // if pool throws a "LIQ_TOO_SMALL" or "LIQ_TOO_LARGE" error, adjust the fill percentage // by 1 percentage point before retrying. if (e instanceof ContractError) { - if ( - e.type === ContractErrorType.InvalidLiqTooSmall && - userLiquidation.liquidationPercent < BigInt(100) - ) { - userLiquidation.liquidationPercent += BigInt(1); - } else if ( - e.type === ContractErrorType.InvalidLiqTooLarge && - userLiquidation.liquidationPercent > BigInt(1) - ) { - userLiquidation.liquidationPercent -= BigInt(1); + if (e.type === ContractErrorType.InvalidLiqTooSmall && auction.auctionPercent < 100) { + auction.auctionPercent += 1; + } else if (e.type === ContractErrorType.InvalidLiqTooLarge && auction.auctionPercent > 1) { + auction.auctionPercent -= 1; } } + const logMessage = - `Error creating user liquidation\n` + - `Pool: ${userLiquidation.poolId}\n` + - `User: ${userLiquidation.user}\n` + - `Liquidation Percent: ${userLiquidation.liquidationPercent}\n` + + `Error creating auction\n` + + `Auction Type: ${AuctionType[auction.auctionType]}\n` + + `Pool: ${auction.poolId}\n` + + `User: ${auction.user}\n` + + `Auction Percent: ${auction.auctionPercent}\n` + + `Bid: ${stringify(auction.bid)}\n` + + `Lot: ${stringify(auction.lot)}\n` + `Error: ${stringify(serializeError(e))}\n`; logger.error(logMessage); await sendSlackNotification(`\n` + logMessage); @@ -130,7 +124,7 @@ export class WorkSubmitter extends SubmissionQueue { logger.info( `Transferring bad debt to backstop for user: ${badDebtTransfer.user} in pool: ${badDebtTransfer.poolId}` ); - const pool = new PoolContractV1(badDebtTransfer.poolId); + const pool = new PoolContractV2(badDebtTransfer.poolId); const op = pool.badDebt(badDebtTransfer.user); await sorobanHelper.submitTransaction(op, APP_CONFIG.keypair); const logMessage = @@ -144,44 +138,7 @@ export class WorkSubmitter extends SubmissionQueue { const logMessage = `Error transfering bad debt\n` + `Pool: ${badDebtTransfer.poolId}\n` + - `User: ${badDebtTransfer.user}`; - logger.error(logMessage, e); - await sendSlackNotification( - ` ` + logMessage + `\nError: ${stringify(serializeError(e))}` - ); - return false; - } - } - - async submitBadDebtAuction( - sorobanHelper: SorobanHelper, - submission: BadDebtAuction - ): Promise { - try { - logger.info(`Creating bad debt auction for pool: ${submission.poolId}`); - - const pool = new PoolContractV1(submission.poolId); - const op = pool.newBadDebtAuction(); - - const auctionExists = - (await sorobanHelper.loadAuction( - submission.poolId, - APP_CONFIG.backstopAddress, - AuctionType.BadDebt - )) !== undefined; - if (auctionExists) { - logger.info(`Bad debt auction already exists for pool: ${submission.poolId}`); - return true; - } - await sorobanHelper.submitTransaction(op, APP_CONFIG.keypair); - const logMessage = `Successfully created bad debt auction\n` + `Pool: ${submission.poolId}`; - logger.info(logMessage); - await sendSlackNotification(logMessage); - return true; - } catch (e: any) { - const logMessage = - `Error creating bad debt auction\n` + - `Pool: ${submission.poolId}` + + `User: ${badDebtTransfer.user}` + `Error: ${stringify(serializeError(e))}\n`; logger.error(logMessage); await sendSlackNotification(` ` + logMessage); @@ -193,9 +150,12 @@ export class WorkSubmitter extends SubmissionQueue { // TODO: Send slack alert for dropped submission let logMessage: string; switch (submission.type) { - case WorkSubmissionType.LiquidateUser: + case WorkSubmissionType.AuctionCreation: logMessage = - `Dropped liquidation\n` + `pool: ${submission.poolId}\n` + `user: ${submission.user}`; + `Dropped auction creation\n` + + `pool: ${submission.poolId}\n` + + `Auction Type: ${submission.auctionType}\n` + + `user: ${submission.user}`; break; case WorkSubmissionType.BadDebtTransfer: logMessage = @@ -203,9 +163,6 @@ export class WorkSubmitter extends SubmissionQueue { `pool: ${submission.poolId}\n` + `user: ${submission.user}`; break; - case WorkSubmissionType.BadDebtAuction: - logMessage = `Dropped bad debt auction\n` + `pool: ${submission.poolId}\n`; - break; } logger.error(logMessage); await sendSlackNotification(logMessage); diff --git a/test/auction.test.ts b/test/auction.test.ts index bb95aa3..039d032 100644 --- a/test/auction.test.ts +++ b/test/auction.test.ts @@ -87,9 +87,9 @@ describe('auctions', () => { totalEffectiveCollateral: 4750, borrowCap: 0, borrowLimit: 0, - netApr: 0, - supplyApr: 0, - borrowApr: 0, + netApy: 0, + supplyApy: 0, + borrowApy: 0, }; mockedSorobanHelper.loadPool.mockResolvedValue(mockPool); mockedSorobanHelper.loadPoolOracle.mockResolvedValue(mockPoolOracle); diff --git a/test/bidder_handler.test.ts b/test/bidder_handler.test.ts index 17255f1..d912d28 100644 --- a/test/bidder_handler.test.ts +++ b/test/bidder_handler.test.ts @@ -2,7 +2,12 @@ import { Auction } from '@blend-capital/blend-sdk'; import { Keypair } from '@stellar/stellar-sdk'; import { AuctionFill, calculateAuctionFill } from '../src/auction'; import { BidderHandler } from '../src/bidder_handler'; -import { AuctionBid, BidderSubmissionType, BidderSubmitter } from '../src/bidder_submitter'; +import { + AddAllowance, + AuctionBid, + BidderSubmissionType, + BidderSubmitter, +} from '../src/bidder_submitter'; import { AppEvent, EventType, LedgerEvent } from '../src/events'; import { APP_CONFIG, AppConfig } from '../src/utils/config'; import { AuctioneerDatabase, AuctionEntry, AuctionType } from '../src/utils/db'; @@ -20,6 +25,8 @@ jest.mock('../src/utils/logger.js', () => ({ })); jest.mock('../src/utils/config.js', () => { let config: AppConfig = { + backstopAddress: 'backstopAddress', + backstopTokenAddress: 'backstopTokenAddress', fillers: [ { name: 'filler1', @@ -476,4 +483,58 @@ describe('BidderHandler', () => { expect(logger.error).toHaveBeenCalledTimes(1); expect(mockedSorobanHelper.loadAuction).not.toHaveBeenCalled(); }); + + it('Checks for allowance if interest auction', async () => { + let ledger = 1000; // nextLedger is 1001 + let auction_1: AuctionEntry = { + pool_id: 'pool1', + user_id: APP_CONFIG.backstopAddress, + auction_type: AuctionType.Interest, + filler: APP_CONFIG.fillers[0].keypair.publicKey(), + start_block: ledger - 150, + fill_block: 0, + updated: 0, + }; + + db.setAuctionEntry(auction_1); + mockedSorobanHelper.loadAuction.mockResolvedValue( + new Auction('teapot', AuctionType.Interest, { + bid: new Map(), + lot: new Map(), + block: ledger - 1, + }) + ); + + let fill_calc_1: AuctionFill = { + block: 1001, + percent: 50, + lotValue: 1000, + bidValue: 900, + requests: [], + }; + mockedCalcAuctionFill.mockResolvedValueOnce(fill_calc_1); + + const appEvent: AppEvent = { + type: EventType.LEDGER, + ledger, + } as LedgerEvent; + await bidderHandler.processEvent(appEvent); + + let new_auction_1 = db.getAuctionEntry( + auction_1.pool_id, + auction_1.user_id, + auction_1.auction_type + ); + expect(new_auction_1?.fill_block).toEqual(fill_calc_1.block); + expect(new_auction_1?.updated).toEqual(ledger); + + let submission_1: AddAllowance = { + filler: APP_CONFIG.fillers[0], + type: BidderSubmissionType.ADD_ALLOWANCE, + assetId: APP_CONFIG.backstopTokenAddress, + spender: APP_CONFIG.backstopAddress, + currLedger: appEvent.ledger, + }; + expect(mockedBidderSubmitter.addSubmission).toHaveBeenCalledWith(submission_1, 4); + }); }); diff --git a/test/bidder_submitter.test.ts b/test/bidder_submitter.test.ts index 1f52aa3..40e1b40 100644 --- a/test/bidder_submitter.test.ts +++ b/test/bidder_submitter.test.ts @@ -2,13 +2,14 @@ import { Auction, PoolUser, Positions, Request, RequestType } from '@blend-capit import { Keypair } from '@stellar/stellar-sdk'; import { AuctionFill, calculateAuctionFill } from '../src/auction'; import { + AddAllowance, AuctionBid, BidderSubmissionType, BidderSubmitter, FillerUnwind, } from '../src/bidder_submitter'; import { getFillerAvailableBalances, managePositions } from '../src/filler'; -import { Filler } from '../src/utils/config'; +import { APP_CONFIG, Filler } from '../src/utils/config'; import { AuctioneerDatabase, AuctionEntry, AuctionType, FilledAuctionEntry } from '../src/utils/db'; import { logger } from '../src/utils/logger'; import { sendSlackNotification } from '../src/utils/slack_notifier'; @@ -41,6 +42,7 @@ jest.mock('../src/utils/config.js', () => { rpcURL: 'http://localhost:8000/rpc', networkPassphrase: 'Public Global Stellar Network ; September 2015', backstopTokenAddress: 'CAS3FL6TLZKDGGSISDBWGGPXT3NRR4DYTZD7YOD3HMYO6LTJUVGRVEAM', + backstopAddress: 'CAS3FL6TLZKDGGSISDBWGGPXT3NRR4DYTZD7YOD3HMYO6LTJUVGRVEAM', usdcAddress: 'CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75', blndAddress: 'CD25MNVTZDL4Y3XBCPCJXGXATV5WUHHOWMYFF4YBEGU5FCPGMYTVG5JY', keypair: '', @@ -338,6 +340,113 @@ describe('BidderSubmitter', () => { expect(bidderSubmitter.containsAuction(auctionEntry)).toBe(false); }); + it('add allowance checks if amount is below threshold', async () => { + bidderSubmitter.addSubmission = jest.fn(); + mockedSorobanHelper.loadAllowance.mockResolvedValue({ + amount: BigInt(100000e7 - 1), + expiration_ledger: 1000 + 17368 * 7, + }); + + const submission: AddAllowance = { + type: BidderSubmissionType.ADD_ALLOWANCE, + filler: { + name: 'test-filler', + keypair: Keypair.random(), + defaultProfitPct: 0, + supportedPools: [ + { + poolAddress: mockPool.id, + primaryAsset: 'USD', + minPrimaryCollateral: 100n, + minHealthFactor: 0, + forceFill: false, + }, + ], + supportedBid: ['USD', 'XLM'], + supportedLot: ['EURC', 'XLM'], + }, + assetId: APP_CONFIG.backstopTokenAddress, + spender: APP_CONFIG.backstopAddress, + currLedger: 1000, + }; + let result = await bidderSubmitter.submit(submission); + + expect(result).toBe(true); + expect(mockedSorobanHelper.submitTransaction).toHaveBeenCalledTimes(1); + expect(bidderSubmitter.addSubmission).toHaveBeenCalledTimes(0); + }); + it('add allowance checks if expiration is below threshold', async () => { + bidderSubmitter.addSubmission = jest.fn(); + mockedSorobanHelper.loadAllowance.mockResolvedValue({ + amount: BigInt('18446744073709551615'), + expiration_ledger: 1000 + 17368 * 7 - 1, + }); + + const submission: AddAllowance = { + type: BidderSubmissionType.ADD_ALLOWANCE, + filler: { + name: 'test-filler', + keypair: Keypair.random(), + defaultProfitPct: 0, + supportedPools: [ + { + poolAddress: mockPool.id, + primaryAsset: 'USD', + minPrimaryCollateral: 100n, + minHealthFactor: 0, + forceFill: false, + }, + ], + supportedBid: ['USD', 'XLM'], + supportedLot: ['EURC', 'XLM'], + }, + assetId: APP_CONFIG.backstopTokenAddress, + spender: APP_CONFIG.backstopAddress, + currLedger: 1000, + }; + let result = await bidderSubmitter.submit(submission); + + expect(result).toBe(true); + expect(mockedSorobanHelper.submitTransaction).toHaveBeenCalledTimes(1); + expect(bidderSubmitter.addSubmission).toHaveBeenCalledTimes(0); + }); + + it('add allowance valid allowance no action taken', async () => { + bidderSubmitter.addSubmission = jest.fn(); + mockedSorobanHelper.loadAllowance.mockResolvedValue({ + amount: BigInt('18446744073709551615'), + expiration_ledger: 1000 + 17368 * 7, + }); + + const submission: AddAllowance = { + type: BidderSubmissionType.ADD_ALLOWANCE, + filler: { + name: 'test-filler', + keypair: Keypair.random(), + defaultProfitPct: 0, + supportedPools: [ + { + poolAddress: mockPool.id, + primaryAsset: 'USD', + minPrimaryCollateral: 100n, + minHealthFactor: 0, + forceFill: false, + }, + ], + supportedBid: ['USD', 'XLM'], + supportedLot: ['EURC', 'XLM'], + }, + assetId: APP_CONFIG.backstopTokenAddress, + spender: APP_CONFIG.backstopAddress, + currLedger: 1000, + }; + let result = await bidderSubmitter.submit(submission); + + expect(result).toBe(true); + expect(mockedSorobanHelper.submitTransaction).toHaveBeenCalledTimes(0); + expect(bidderSubmitter.addSubmission).toHaveBeenCalledTimes(0); + }); + it('should handle dropped bid', async () => { const submission: AuctionBid = { type: BidderSubmissionType.BID, @@ -420,4 +529,46 @@ describe('BidderSubmitter', () => { `Dropped filler unwind\n` + `Filler: ${submission.filler.name}\n` + `Pool: ${mockPool.id}` ); }); + + it('should handle dropped add allowance', async () => { + const submission: AddAllowance = { + type: BidderSubmissionType.ADD_ALLOWANCE, + filler: { + name: 'test-filler', + keypair: Keypair.random(), + defaultProfitPct: 0, + supportedPools: [ + { + poolAddress: mockPool.id, + primaryAsset: 'USD', + minPrimaryCollateral: 100n, + minHealthFactor: 0, + forceFill: false, + }, + ], + supportedBid: ['USD', 'XLM'], + supportedLot: ['EURC', 'XLM'], + }, + assetId: APP_CONFIG.backstopTokenAddress, + spender: APP_CONFIG.backstopAddress, + currLedger: 1000, + }; + + await bidderSubmitter.onDrop(submission); + + expect(logger.error).toHaveBeenCalledWith( + `Dropped allowance check\n` + + `Filler: ${submission.filler.name}\n` + + `Spender: ${submission.spender}\n` + + `Asset: ${submission.assetId}\n` + + `Ledger: ${submission.currLedger}` + ); + expect(mockedSendSlackNotif).toHaveBeenCalledWith( + `Dropped allowance check\n` + + `Filler: ${submission.filler.name}\n` + + `Spender: ${submission.spender}\n` + + `Asset: ${submission.assetId}\n` + + `Ledger: ${submission.currLedger}` + ); + }); }); diff --git a/test/helpers/mock-pool.json b/test/helpers/mock-pool.json index 22446c4..28a7902 100644 --- a/test/helpers/mock-pool.json +++ b/test/helpers/mock-pool.json @@ -50,20 +50,22 @@ "r_one": 500000, "r_two": 5000000, "r_three": 50000000, - "reactivity": 20 + "reactivity": 20, + "supply_cap": 170141183460469231731687303715884105727, + "enabled": true }, "data": { "dRate": { "type": "bigint", - "value": "1000568954" + "value": "1000568954000" }, "bRate": { "type": "bigint", - "value": "1000087669" + "value": "1000087669000" }, "interestRateModifier": { "type": "bigint", - "value": "100000000" + "value": "1000000" }, "dSupply": { "type": "bigint", @@ -80,37 +82,29 @@ "lastTime": 1723574859 }, "borrowEmissions": { - "config": { - "eps": { - "type": "bigint", - "value": "93079" - }, - "expiration": 1724136187 - }, - "data": { - "index": { - "type": "bigint", - "value": "5978351" - }, - "lastTime": 1723565204 + "eps": { + "type": "bigint", + "value": "93079" + }, + "expiration": 1724136187, + "index": { + "type": "bigint", + "value": "5978351" }, + "lastTime": 1723565204, "latestLedger": 53017264 }, "supplyEmissions": { - "config": { - "eps": { - "type": "bigint", - "value": "93079" - }, - "expiration": 1724136187 - }, - "data": { - "index": { - "type": "bigint", - "value": "79988" - }, - "lastTime": 1723574939 + "eps": { + "type": "bigint", + "value": "93079" + }, + "expiration": 1724136187, + "index": { + "type": "bigint", + "value": "79988" }, + "lastTime": 1723574939, "latestLedger": 53017264 }, "borrowApr": 0.0034092, @@ -140,20 +134,22 @@ "r_one": 600000, "r_two": 1200000, "r_three": 50000000, - "reactivity": 20 + "reactivity": 20, + "supply_cap": 170141183460469231731687303715884105727, + "enabled": true }, "data": { "dRate": { "type": "bigint", - "value": "1010182553" + "value": "1010182553000" }, "bRate": { "type": "bigint", - "value": "1006990441" + "value": "1006990441000" }, "interestRateModifier": { "type": "bigint", - "value": "1195678360" + "value": "11956783" }, "dSupply": { "type": "bigint", @@ -170,37 +166,29 @@ "lastTime": 1723567543 }, "borrowEmissions": { - "config": { - "eps": { - "type": "bigint", - "value": "232700" - }, - "expiration": 1724136187 - }, - "data": { - "index": { - "type": "bigint", - "value": "1668489" - }, - "lastTime": 1723574728 + "eps": { + "type": "bigint", + "value": "232700" + }, + "expiration": 1724136187, + "index": { + "type": "bigint", + "value": "1668489" }, + "lastTime": 1723574728, "latestLedger": 53017264 }, "supplyEmissions": { - "config": { - "eps": { - "type": "bigint", - "value": "232700" - }, - "expiration": 1724136187 - }, - "data": { - "index": { - "type": "bigint", - "value": "1336228" - }, - "lastTime": 1723574728 + "eps": { + "type": "bigint", + "value": "232700" }, + "expiration": 1724136187, + "index": { + "type": "bigint", + "value": "1336228" + }, + "lastTime": 1723574728, "latestLedger": 53017264 }, "borrowApr": 0.1500306, @@ -230,20 +218,22 @@ "r_one": 600000, "r_two": 1200000, "r_three": 50000000, - "reactivity": 20 + "reactivity": 20, + "supply_cap": 170141183460469231731687303715884105727, + "enabled": true }, "data": { "dRate": { "type": "bigint", - "value": "1009888396" + "value": "1009888396000" }, "bRate": { "type": "bigint", - "value": "1004770254" + "value": "1004770254000" }, "interestRateModifier": { "type": "bigint", - "value": "1256459879" + "value": "12564598" }, "dSupply": { "type": "bigint", @@ -260,37 +250,29 @@ "lastTime": 1723567988 }, "borrowEmissions": { - "config": { - "eps": { - "type": "bigint", - "value": "93079" - }, - "expiration": 1724136187 - }, - "data": { - "index": { - "type": "bigint", - "value": "3843472" - }, - "lastTime": 1723573325 + "eps": { + "type": "bigint", + "value": "93079" }, + "expiration": 1724136187, + "index": { + "type": "bigint", + "value": "3843472" + }, + "lastTime": 1723573325, "latestLedger": 53017264 }, "supplyEmissions": { - "config": { - "eps": { - "type": "bigint", - "value": "93079" - }, - "expiration": 1724136187 - }, - "data": { - "index": { - "type": "bigint", - "value": "2190400" - }, - "lastTime": 1723573325 + "eps": { + "type": "bigint", + "value": "93079" }, + "expiration": 1724136187, + "index": { + "type": "bigint", + "value": "2190400" + }, + "lastTime": 1723573325, "latestLedger": 53017264 }, "borrowApr": 0.1104193, @@ -320,20 +302,22 @@ "r_one": 600000, "r_two": 3000000, "r_three": 50000000, - "reactivity": 20 + "reactivity": 20, + "supply_cap": 170141183460469231731687303715884105727, + "enabled": true }, "data": { "dRate": { "type": "bigint", - "value": "1000360584" + "value": "1000360584000" }, "bRate": { "type": "bigint", - "value": "1000024989" + "value": "1000024989000" }, "interestRateModifier": { "type": "bigint", - "value": "100000000" + "value": "1000000" }, "dSupply": { "type": "bigint", @@ -350,20 +334,16 @@ "lastTime": 1723571395 }, "supplyEmissions": { - "config": { - "eps": { - "type": "bigint", - "value": "93079" - }, - "expiration": 1724136187 - }, - "data": { - "index": { - "type": "bigint", - "value": "1716" - }, - "lastTime": 1723571395 + "eps": { + "type": "bigint", + "value": "93079" + }, + "expiration": 1724136187, + "index": { + "type": "bigint", + "value": "1716" }, + "lastTime": 1723571395, "latestLedger": 53017265 }, "borrowApr": 0.0066767, diff --git a/test/helpers/mocks.ts b/test/helpers/mocks.ts index 9bb0f18..9595d98 100644 --- a/test/helpers/mocks.ts +++ b/test/helpers/mocks.ts @@ -1,4 +1,4 @@ -import { PoolOracle, PoolV1, PriceData, ReserveV1 } from '@blend-capital/blend-sdk'; +import { Pool, PoolOracle, PoolV2, PriceData, ReserveV2 } from '@blend-capital/blend-sdk'; import Database from 'better-sqlite3'; import * as fs from 'fs'; import * as path from 'path'; @@ -6,23 +6,23 @@ import { AuctioneerDatabase } from '../../src/utils/db.js'; import { parse } from '../../src/utils/json.js'; const mockPoolPath = path.resolve(__dirname, 'mock-pool.json'); -let pool = parse(fs.readFileSync(mockPoolPath, 'utf8')); +let pool = parse(fs.readFileSync(mockPoolPath, 'utf8')); pool.reserves.forEach((reserve, assetId, map) => { - const reserveV1 = reserve as ReserveV1; - map.set( + let parsedReserve = new ReserveV2( + pool.id, assetId, - new ReserveV1( - pool.id, - assetId, - reserve.config, - reserve.data, - reserveV1.borrowEmissions, - reserveV1.supplyEmissions, - reserve.borrowApr, - reserve.supplyApr, - reserve.latestLedger - ) + reserve.config, + reserve.data, + reserve.borrowEmissions, + reserve.supplyEmissions, + reserve.borrowApr, + 0, + reserve.supplyApr, + 0, + reserve.latestLedger ); + parsedReserve.setRates(BigInt(pool.metadata.backstopRate)); + map.set(assetId, parsedReserve); }); export let mockPool = pool; diff --git a/test/liquidations.test.ts b/test/liquidations.test.ts index 867035b..fb366b4 100644 --- a/test/liquidations.test.ts +++ b/test/liquidations.test.ts @@ -1,4 +1,10 @@ -import { Auction, PoolUser, Positions, PositionsEstimate } from '@blend-capital/blend-sdk'; +import { + Auction, + AuctionType, + PoolUser, + Positions, + PositionsEstimate, +} from '@blend-capital/blend-sdk'; import { Keypair } from '@stellar/stellar-sdk'; import { calculateLiquidationPercent, @@ -11,7 +17,18 @@ import { APP_CONFIG } from '../src/utils/config.js'; import { AuctioneerDatabase } from '../src/utils/db.js'; import { PoolUserEst, SorobanHelper } from '../src/utils/soroban_helper.js'; import { WorkSubmissionType } from '../src/work_submitter.js'; -import { inMemoryAuctioneerDb, mockPool, USDC, USDC_ID, XLM_ID } from './helpers/mocks.js'; +import { + AQUA_ID, + EURC, + EURC_ID, + inMemoryAuctioneerDb, + mockPool, + mockPoolOracle, + USDC, + USDC_ID, + XLM, + XLM_ID, +} from './helpers/mocks.js'; jest.mock('../src/utils/soroban_helper.js'); jest.mock('../src/utils/logger.js', () => ({ @@ -24,6 +41,7 @@ jest.mock('../src/utils/config.js', () => { return { APP_CONFIG: { backstopAddress: 'backstopAddress', + backstopTokenAddress: 'backstopTokenAddress', pools: ['pool1', 'pool2'], }, }; @@ -300,7 +318,7 @@ describe('checkUsersForLiquidationsAndBadDebt', () => { mockBackstopPositionsEstimate.totalEffectiveCollateral = 0; mockBackstopPositions.positions = new Positions( new Map([[USDC_ID, 2000n]]), - new Map([[XLM_ID, 3000n]]), + new Map(), new Map() ); mockedSorobanHelper.loadUserPositionEstimate.mockResolvedValue({ @@ -318,8 +336,13 @@ describe('checkUsersForLiquidationsAndBadDebt', () => { expect(result).toEqual([ { - type: WorkSubmissionType.BadDebtAuction, + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, + user: APP_CONFIG.backstopAddress, + auctionType: AuctionType.BadDebt, + bid: [USDC], + lot: [APP_CONFIG.backstopTokenAddress], + auctionPercent: 100, }, ]); }); @@ -352,10 +375,13 @@ describe('checkUsersForLiquidationsAndBadDebt', () => { expect(result.length).toBe(1); expect(result).toEqual([ { - type: WorkSubmissionType.LiquidateUser, + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, + auctionType: AuctionType.Liquidation, user: 'user1', - liquidationPercent: 56n, + auctionPercent: 56, + bid: [USDC], + lot: [XLM], }, ]); }); diff --git a/test/pool_event_handler.test.ts b/test/pool_event_handler.test.ts index 18785a6..014e470 100644 --- a/test/pool_event_handler.test.ts +++ b/test/pool_event_handler.test.ts @@ -1,4 +1,5 @@ import { + AuctionData, BlendContractType, FixedMath, PoolEventType, @@ -136,6 +137,8 @@ describe('poolEventHandler', () => { bid: new Map(), lot: new Map(), }, + user: APP_CONFIG.backstopAddress, + percent: 100, }, }; await poolEventHandler.processEventWithRetryAndDeadLetter(poolEvent); @@ -161,6 +164,8 @@ describe('poolEventHandler', () => { bid: new Map(), lot: new Map(), }, + user: APP_CONFIG.backstopAddress, + percent: 100, }, }; let error = new Error('Temporary error'); @@ -193,6 +198,8 @@ describe('poolEventHandler', () => { bid: new Map(), lot: new Map(), }, + user: APP_CONFIG.backstopAddress, + percent: 100, }, }; mockedSorobanHelper.loadPool.mockRejectedValue(new Error('Permanent error')); @@ -220,6 +227,8 @@ describe('poolEventHandler', () => { bid: new Map(), lot: new Map(), }, + user: APP_CONFIG.backstopAddress, + percent: 100, }, }; @@ -353,13 +362,15 @@ describe('poolEventHandler', () => { ledger, ledgerClosedAt: '2021-10-01T00:00:00Z', txHash: '0x123', - eventType: PoolEventType.NewLiquidationAuction, + eventType: PoolEventType.NewAuction, auctionData: { bid: new Map([['ETH', BigInt(123)]]), lot: new Map([['XLM', BigInt(456)]]), block: 500, }, user: user, + auctionType: AuctionType.Liquidation, + percent: 100, }, }; @@ -396,6 +407,8 @@ describe('poolEventHandler', () => { block: 500, }, auctionType: AuctionType.Interest, + user: APP_CONFIG.backstopAddress, + percent: 100, }, }; @@ -436,6 +449,8 @@ describe('poolEventHandler', () => { block: 500, }, auctionType: AuctionType.BadDebt, + user: APP_CONFIG.backstopAddress, + percent: 100, }, }; @@ -471,13 +486,15 @@ describe('poolEventHandler', () => { ledger, ledgerClosedAt: '2021-10-01T00:00:00Z', txHash: '0x123', - eventType: PoolEventType.NewLiquidationAuction, + eventType: PoolEventType.NewAuction, auctionData: { bid: new Map([['ETH', BigInt(123)]]), lot: new Map([['BTC', BigInt(456)]]), block: 500, }, user: user, + auctionType: AuctionType.Liquidation, + percent: 100, }, }; @@ -578,7 +595,8 @@ describe('poolEventHandler', () => { user: pool_user, auctionType: AuctionType.Liquidation, fillAmount: BigInt(99), - from: Keypair.random().publicKey(), + filler: Keypair.random().publicKey(), + filledAuctionData: new AuctionData(new Map(), 1, new Map()), }, }; @@ -601,7 +619,8 @@ describe('poolEventHandler', () => { user: pool_user, auctionType: AuctionType.Liquidation, fillAmount: BigInt(100), - from: Keypair.random().publicKey(), + filler: Keypair.random().publicKey(), + filledAuctionData: new AuctionData(new Map(), 1, new Map()), }, }; @@ -654,7 +673,8 @@ describe('poolEventHandler', () => { user: APP_CONFIG.backstopAddress, auctionType: AuctionType.Interest, fillAmount: BigInt(100), - from: Keypair.random().publicKey(), + filler: Keypair.random().publicKey(), + filledAuctionData: new AuctionData(new Map(), 1, new Map()), }, }; @@ -721,7 +741,8 @@ describe('poolEventHandler', () => { user: APP_CONFIG.backstopAddress, auctionType: AuctionType.BadDebt, fillAmount: BigInt(100), - from: Keypair.random().publicKey(), + filler: Keypair.random().publicKey(), + filledAuctionData: new AuctionData(new Map(), 1, new Map()), }, }; diff --git a/test/utils/json.test.ts b/test/utils/json.test.ts index 0ed360d..ada206f 100644 --- a/test/utils/json.test.ts +++ b/test/utils/json.test.ts @@ -1,4 +1,4 @@ -import { BlendContractType, PoolEventType, PoolNewAuctionV1Event } from '@blend-capital/blend-sdk'; +import { BlendContractType, PoolEventType, PoolNewAuctionV2Event } from '@blend-capital/blend-sdk'; import { EventType, PoolEventEvent } from '../../src/events.js'; import { UserEntry } from '../../src/utils/db.js'; import { parse, stringify } from '../../src/utils/json.js'; @@ -64,7 +64,7 @@ test('user entry parse round trip', () => { }); test('blend event parse round trip', () => { - const blendEvent: PoolNewAuctionV1Event = { + const blendEvent: PoolNewAuctionV2Event = { id: 'abc', contractId: '123', contractType: BlendContractType.Pool, @@ -72,6 +72,8 @@ test('blend event parse round trip', () => { ledgerClosedAt: Date.now().toLocaleString(), txHash: '0x123', eventType: PoolEventType.NewAuction, + user: 'user', + percent: 50, auctionType: 2, auctionData: { bid: new Map([['C2', BigInt(123)]]), @@ -108,6 +110,10 @@ test('blend event parse round trip', () => { expect(eventTest.event.txHash).toEqual(asObj.event.txHash); expect(eventTest.event.eventType).toEqual(asObj.event.eventType); expect(eventTest.event.auctionType).toEqual(asObj.event.auctionType); + if ('percent' in eventTest.event && 'percent' in asObj.event) { + expect(eventTest.event.user).toContain(asObj.event.user); + expect(eventTest.event.percent).toEqual(asObj.event.percent); + } expect(eventTest.event.auctionData.bid.size).toEqual(asObj.event.auctionData.bid.size); expect(eventTest.event.auctionData.bid.get('C2')).toEqual( asObj.event.auctionData.bid.get('C2') diff --git a/test/work_handler.test.ts b/test/work_handler.test.ts index 9122479..165c8b0 100644 --- a/test/work_handler.test.ts +++ b/test/work_handler.test.ts @@ -2,7 +2,7 @@ import { PoolOracle } from '@blend-capital/blend-sdk'; import { AppEvent, EventType, OracleScanEvent } from '../src/events'; import { checkUsersForLiquidationsAndBadDebt } from '../src/liquidations'; import { OracleHistory } from '../src/oracle_history'; -import { AuctioneerDatabase, UserEntry } from '../src/utils/db'; +import { AuctioneerDatabase, AuctionType, UserEntry } from '../src/utils/db'; import { SorobanHelper } from '../src/utils/soroban_helper'; import { WorkHandler } from '../src/work_handler'; import { WorkSubmission, WorkSubmissionType, WorkSubmitter } from '../src/work_submitter'; @@ -71,7 +71,7 @@ describe('WorkHandler', () => { ]; const usersWithCollateral: UserEntry[] = [ { - pool_id: 'pool1', + pool_id: 'pool2', user_id: 'user1', health_factor: 0, collateral: new Map([['asset2', BigInt(100)]]), @@ -83,14 +83,20 @@ describe('WorkHandler', () => { { poolId: APP_CONFIG.pools[0], user: 'user1', - type: WorkSubmissionType.LiquidateUser, - liquidationPercent: 10n, + type: WorkSubmissionType.AuctionCreation, + auctionType: AuctionType.Liquidation, + bid: ['asset1'], + lot: ['asset2'], + auctionPercent: 10, }, { - poolId: APP_CONFIG.pools[0], + poolId: APP_CONFIG.pools[1], user: 'user1', - type: WorkSubmissionType.LiquidateUser, - liquidationPercent: 10n, + type: WorkSubmissionType.AuctionCreation, + auctionType: AuctionType.Liquidation, + bid: ['asset1'], + lot: ['asset2'], + auctionPercent: 10, }, ]; sorobanHelper.loadPoolOracle.mockResolvedValue(poolOracle); diff --git a/test/work_submitter.test.ts b/test/work_submitter.test.ts index 3e3095b..fa4c3a9 100644 --- a/test/work_submitter.test.ts +++ b/test/work_submitter.test.ts @@ -6,7 +6,8 @@ import { logger } from '../src/utils/logger'; import { sendSlackNotification } from '../src/utils/slack_notifier'; import { SorobanHelper } from '../src/utils/soroban_helper'; import { WorkSubmission, WorkSubmissionType, WorkSubmitter } from '../src/work_submitter'; -import { mockPool, USDC } from './helpers/mocks'; +import { mockPool } from './helpers/mocks'; +import { serializeError, stringify } from '../src/utils/json'; // Mock dependencies jest.mock('../src/utils/db'); @@ -45,11 +46,14 @@ describe('WorkSubmitter', () => { it('should submit a user liquidation successfully', async () => { mockedSorobanHelper.loadAuction.mockResolvedValue(undefined); - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(50), + auctionType: AuctionType.Liquidation, + lot: [], + bid: [], + auctionPercent: 50, }; const result = await workSubmitter.submit(submission); @@ -72,11 +76,14 @@ describe('WorkSubmitter', () => { }) ); - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(50), + auctionType: AuctionType.Liquidation, + lot: [], + bid: [], + auctionPercent: 50, }; const result = await workSubmitter.submit(submission); @@ -92,18 +99,19 @@ describe('WorkSubmitter', () => { new ContractError(ContractErrorType.InvalidLiqTooSmall) ); - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(50), + auctionType: AuctionType.Liquidation, + auctionPercent: 50, lot: [], bid: [], }; const result = await workSubmitter.submit(submission); expect(result).toBe(false); - expect(submission.liquidationPercent).toBe(BigInt(51)); + expect(submission.auctionPercent).toBe(51); expect(logger.error).toHaveBeenCalled(); expect(mockedSendSlackNotif).toHaveBeenCalled(); }); @@ -114,16 +122,19 @@ describe('WorkSubmitter', () => { new ContractError(ContractErrorType.InvalidLiqTooSmall) ); - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(100), + auctionType: AuctionType.Liquidation, + auctionPercent: 100, + lot: [], + bid: [], }; const result = await workSubmitter.submit(submission); expect(result).toBe(false); - expect(submission.liquidationPercent).toBe(BigInt(100)); + expect(submission.auctionPercent).toBe(100); expect(logger.error).toHaveBeenCalled(); expect(mockedSendSlackNotif).toHaveBeenCalled(); }); @@ -134,36 +145,42 @@ describe('WorkSubmitter', () => { new ContractError(ContractErrorType.InvalidLiqTooLarge) ); - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(50), + auctionType: AuctionType.Liquidation, + auctionPercent: 50, + lot: [], + bid: [], }; const result = await workSubmitter.submit(submission); expect(result).toBe(false); - expect(submission.liquidationPercent).toBe(BigInt(49)); + expect(submission.auctionPercent).toBe(49); expect(logger.error).toHaveBeenCalled(); expect(mockedSendSlackNotif).toHaveBeenCalled(); }); - it('does not increase fill percentage past below 1 for user liquidation with error LIQ_TOO_LARGE', async () => { + it('does not decrease fill percentage past below 1 for user liquidation with error LIQ_TOO_LARGE', async () => { mockedSorobanHelper.loadAuction.mockResolvedValue(undefined); mockedSorobanHelper.submitTransaction.mockRejectedValue( new ContractError(ContractErrorType.InvalidLiqTooLarge) ); - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(1), + auctionType: AuctionType.Liquidation, + auctionPercent: 1, + lot: [], + bid: [], }; const result = await workSubmitter.submit(submission); expect(result).toBe(false); - expect(submission.liquidationPercent).toBe(BigInt(1)); + expect(submission.auctionPercent).toBe(1); expect(logger.error).toHaveBeenCalled(); expect(mockedSendSlackNotif).toHaveBeenCalled(); }); @@ -174,16 +191,19 @@ describe('WorkSubmitter', () => { new ContractError(ContractErrorType.InvalidLiquidation) ); - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(50), + auctionType: AuctionType.Liquidation, + auctionPercent: 50, + lot: [], + bid: [], }; const result = await workSubmitter.submit(submission); expect(result).toBe(false); - expect(submission.liquidationPercent).toBe(BigInt(50)); + expect(submission.auctionPercent).toBe(50); expect(logger.error).toHaveBeenCalled(); expect(mockedSendSlackNotif).toHaveBeenCalled(); }); @@ -194,11 +214,14 @@ describe('WorkSubmitter', () => { new ContractError(ContractErrorType.InvalidLiqTooSmall) ); - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(50), + auctionType: AuctionType.Liquidation, + auctionPercent: 50, + lot: [], + bid: [], }; workSubmitter.addSubmission(submission, 3, 0); @@ -206,10 +229,17 @@ describe('WorkSubmitter', () => { await new Promise((resolve) => setTimeout(resolve, 50)); } // 3 retries plus the final increment before dropping - expect(submission.liquidationPercent).toBe(BigInt(54)); + expect(submission.auctionPercent).toBe(54); expect(logger.error).toHaveBeenCalledWith( expect.stringContaining( - 'Error creating user liquidation\n' + `Pool: ${mockPool.id}\n` + `User: ${submission.user}` + 'Error creating auction\n' + + `Auction Type: ${AuctionType[submission.auctionType]}\n` + + `Pool: ${mockPool.id}\n` + + `User: ${submission.user}\n` + + `Auction Percent: ${submission.auctionPercent}\n` + + `Bid: ${stringify(submission.bid)}\n` + + `Lot: ${stringify(submission.lot)}\n` + + `Error: ${stringify(serializeError(new ContractError(ContractErrorType.InvalidLiqTooSmall)))}\n` ) ); }); @@ -232,8 +262,13 @@ describe('WorkSubmitter', () => { mockedSorobanHelper.loadAuction.mockResolvedValue(undefined); const submission: WorkSubmission = { - type: WorkSubmissionType.BadDebtAuction, + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, + user: Keypair.random().publicKey(), + auctionType: AuctionType.BadDebt, + auctionPercent: 50, + lot: [], + bid: [], }; const result = await workSubmitter.submit(submission); @@ -253,8 +288,13 @@ describe('WorkSubmitter', () => { ); const submission: WorkSubmission = { - type: WorkSubmissionType.BadDebtAuction, + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, + user: Keypair.random().publicKey(), + auctionType: AuctionType.BadDebt, + auctionPercent: 50, + lot: [], + bid: [], }; const result = await workSubmitter.submit(submission); @@ -264,11 +304,12 @@ describe('WorkSubmitter', () => { }); it('should log an error when a liquidation is dropped', () => { - const submission = { - type: WorkSubmissionType.LiquidateUser, + const submission: WorkSubmission = { + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, user: Keypair.random().publicKey(), - liquidationPercent: BigInt(50), + auctionType: AuctionType.Liquidation, + auctionPercent: 50, lot: [], bid: [], }; @@ -276,7 +317,10 @@ describe('WorkSubmitter', () => { expect(logger.error).toHaveBeenCalledWith( expect.stringContaining( - 'Dropped liquidation\n' + `pool: ${mockPool.id}\n` + `user: ${submission.user}` + 'Dropped auction creation\n' + + `pool: ${mockPool.id}\n` + + `Auction Type: ${submission.auctionType}\n` + + `user: ${submission.user}` ) ); }); @@ -299,14 +343,23 @@ describe('WorkSubmitter', () => { it('should log an error when a bad debt auction is dropped', () => { const submission: WorkSubmission = { - type: WorkSubmissionType.BadDebtAuction, + type: WorkSubmissionType.AuctionCreation, poolId: mockPool.id, + user: Keypair.random().publicKey(), + auctionType: AuctionType.Liquidation, + auctionPercent: 50, + lot: [], + bid: [], }; - workSubmitter.onDrop(submission); expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining('Dropped bad debt auction\n' + `pool: ${mockPool.id}`) + expect.stringContaining( + 'Dropped auction creation\n' + + `pool: ${mockPool.id}\n` + + `Auction Type: ${submission.auctionType}\n` + + `user: ${submission.user}` + ) ); }); });