Skip to content

Commit 45bb3c2

Browse files
authored
Merge pull request #241 from Olamidepy/feat/issue-174-auction-protocols
feat: implement auction (English/Dutch) computation tools (#174)
2 parents 300fe4a + 819b395 commit 45bb3c2

4 files changed

Lines changed: 302 additions & 0 deletions

File tree

‎src/index.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { sorobanMath } from './tools/soroban_math.js';
2828
import { decodeLedgerEntryTool, decodeLedgerEntrySchema } from './tools/decode_ledger_entry.js';
2929
import { computeVestingSchedule } from './tools/compute_vesting_schedule.js';
3030
import { deployContract } from './tools/deploy_contract.js';
31+
import { calculateDutchAuctionPrice, calculateEnglishAuctionState } from './tools/auction_compute.js';
3132
import { manageDaoTreasury } from './tools/manage_dao_treasury.js';
3233
import { computeInterestRates, calculateBorrowingCapacity } from './tools/lending_compute.js';
3334
import { trackLedgerConsensusTime } from './tools/track_ledger_consensus_time.js';
@@ -62,6 +63,9 @@ import {
6263
SorobanMathInputSchema,
6364
ComputeVestingScheduleInputSchema,
6465
DeployContractInputSchema,
66+
CalculateDutchAuctionPriceInputSchema,
67+
CalculateEnglishAuctionStateInputSchema,
68+
} from './schemas/tools.js';
6569
ManageDaoTreasuryInputSchema,
6670
ComputeInterestRatesInputSchema,
6771
CalculateBorrowingCapacityInputSchema,
@@ -706,6 +710,37 @@ class PulsarServer {
706710
},
707711
},
708712
{
713+
name: 'calculate_dutch_auction_price',
714+
description: 'Calculate the current price of an asset in a Dutch auction (linear price decay). Useful for NFT drops or fair price discovery.',
715+
inputSchema: {
716+
type: 'object',
717+
properties: {
718+
start_price: { type: 'number', description: 'Initial auction price.' },
719+
reserve_price: { type: 'number', description: 'Minimum/floor price.' },
720+
start_timestamp: { type: 'number', description: 'Unix timestamp when decay begins.' },
721+
end_timestamp: { type: 'number', description: 'Unix timestamp when price reaches reserve.' },
722+
current_timestamp: { type: 'number', description: 'Optional override for current time.' },
723+
},
724+
required: ['start_price', 'reserve_price', 'start_timestamp', 'end_timestamp'],
725+
},
726+
},
727+
{
728+
name: 'calculate_english_auction_state',
729+
description: 'Calculate the next bid requirements and state for an English auction.',
730+
inputSchema: {
731+
type: 'object',
732+
properties: {
733+
current_highest_bid: { type: 'number', description: 'Current top bid (0 if no bids).' },
734+
reserve_price: { type: 'number', description: 'Minimum bid to win/start.' },
735+
bid_increment: { type: 'number', description: 'Required increase over the current bid.' },
736+
bid_increment_type: { type: 'string', enum: ['absolute', 'percentage'], default: 'absolute' },
737+
end_timestamp: { type: 'number', description: 'Unix timestamp when auction ends.' },
738+
current_timestamp: { type: 'number', description: 'Optional override for current time.' },
739+
},
740+
required: ['current_highest_bid', 'reserve_price', 'bid_increment', 'end_timestamp'],
741+
},
742+
},
743+
],
709744
name: 'compute_interest_rates',
710745
description: 'Calculate borrow and supply interest rates using a Jump Rate Model (standard for protocols like Aave). Useful for simulating lending pool dynamics.',
711746
inputSchema: {
@@ -1496,6 +1531,9 @@ class PulsarServer {
14961531
case 'get_account_balance': {
14971532
const parsed = GetAccountBalanceInputSchema.safeParse(args);
14981533
if (!parsed.success) {
1534+
throw new PulsarValidationError(`Invalid input for get_account_balance`, parsed.error.format());
1535+
}
1536+
const result = await getAccountBalance(parsed.data);
14991537
throw new PulsarValidationError(
15001538
`Invalid input for get_account_balance`,
15011539
parsed.error.format()
@@ -2053,6 +2091,21 @@ class PulsarServer {
20532091
};
20542092
}
20552093

2094+
case 'calculate_dutch_auction_price': {
2095+
const parsed = CalculateDutchAuctionPriceInputSchema.safeParse(args);
2096+
if (!parsed.success) {
2097+
throw new PulsarValidationError(`Invalid input for calculate_dutch_auction_price`, parsed.error.format());
2098+
}
2099+
const result = await calculateDutchAuctionPrice(parsed.data);
2100+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
2101+
}
2102+
2103+
case 'calculate_english_auction_state': {
2104+
const parsed = CalculateEnglishAuctionStateInputSchema.safeParse(args);
2105+
if (!parsed.success) {
2106+
throw new PulsarValidationError(`Invalid input for calculate_english_auction_state`, parsed.error.format());
2107+
}
2108+
const result = await calculateEnglishAuctionState(parsed.data);
20562109
case 'compute_interest_rates': {
20572110
const parsed = ComputeInterestRatesInputSchema.safeParse(args);
20582111
if (!parsed.success) {

‎src/schemas/tools.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,52 @@ export const EmergencyPauseInputSchema = z.object({
221221

222222
export type EmergencyPauseInput = z.infer<typeof EmergencyPauseInputSchema>;
223223

224+
/**
225+
* Schema for calculate_dutch_auction_price tool
226+
*
227+
* Inputs:
228+
* - start_price: Initial price of the asset
229+
* - reserve_price: Minimum price (bottom of the curve)
230+
* - start_timestamp: Unix timestamp when price begins to decay
231+
* - end_timestamp: Unix timestamp when price reaches reserve
232+
* - current_timestamp: Optional override for current time
233+
*/
234+
export const CalculateDutchAuctionPriceInputSchema = z.object({
235+
start_price: z.number().positive(),
236+
reserve_price: z.number().positive(),
237+
start_timestamp: z.number().int().positive(),
238+
end_timestamp: z.number().int().positive(),
239+
current_timestamp: z.number().int().positive().optional(),
240+
});
241+
242+
export type CalculateDutchAuctionPriceInput = z.infer<
243+
typeof CalculateDutchAuctionPriceInputSchema
244+
>;
245+
246+
/**
247+
* Schema for calculate_english_auction_state tool
248+
*
249+
* Inputs:
250+
* - current_highest_bid: The current bid to beat (0 if none)
251+
* - reserve_price: Minimum bid required to start or win
252+
* - bid_increment: Minimum amount or percentage to add to the highest bid
253+
* - bid_increment_type: 'absolute' | 'percentage'
254+
* - end_timestamp: Unix timestamp when the auction ends
255+
* - current_timestamp: Optional override for current time
256+
*/
257+
export const CalculateEnglishAuctionStateInputSchema = z.object({
258+
current_highest_bid: z.number().nonnegative(),
259+
reserve_price: z.number().positive(),
260+
bid_increment: z.number().positive(),
261+
bid_increment_type: z.enum(["absolute", "percentage"]).default("absolute"),
262+
end_timestamp: z.number().int().positive(),
263+
current_timestamp: z.number().int().positive().optional(),
264+
});
265+
266+
export type CalculateEnglishAuctionStateInput = z.infer<
267+
typeof CalculateEnglishAuctionStateInputSchema
268+
>;
269+
224270
/**
225271
* Schema for generate_contract_docs tool
226272
*

‎src/tools/auction_compute.ts‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
CalculateDutchAuctionPriceInput,
3+
CalculateEnglishAuctionStateInput
4+
} from "../schemas/tools.js";
5+
6+
/**
7+
* Calculates the current price of an asset in a Dutch auction with linear decay.
8+
*/
9+
export async function calculateDutchAuctionPrice(input: CalculateDutchAuctionPriceInput) {
10+
const {
11+
start_price,
12+
reserve_price,
13+
start_timestamp,
14+
end_timestamp,
15+
current_timestamp = Math.floor(Date.now() / 1000)
16+
} = input;
17+
18+
if (current_timestamp <= start_timestamp) {
19+
return {
20+
status: "UPCOMING",
21+
current_price: start_price.toFixed(7),
22+
time_until_start: start_timestamp - current_timestamp,
23+
};
24+
}
25+
26+
if (current_timestamp >= end_timestamp) {
27+
return {
28+
status: "ENDED",
29+
current_price: reserve_price.toFixed(7),
30+
time_since_end: current_timestamp - end_timestamp,
31+
};
32+
}
33+
34+
// Linear decay: Price = Start - (Elapsed / TotalDuration) * (Start - Reserve)
35+
const elapsed = current_timestamp - start_timestamp;
36+
const total_duration = end_timestamp - start_timestamp;
37+
const price_drop = (elapsed / total_duration) * (start_price - reserve_price);
38+
const current_price = start_price - price_drop;
39+
40+
return {
41+
status: "ACTIVE",
42+
current_price: current_price.toFixed(7),
43+
elapsed_seconds: elapsed,
44+
remaining_seconds: end_timestamp - current_timestamp,
45+
progress_percentage: ((elapsed / total_duration) * 100).toFixed(2),
46+
};
47+
}
48+
49+
/**
50+
* Calculates the next bid requirements for an English auction.
51+
*/
52+
export async function calculateEnglishAuctionState(input: CalculateEnglishAuctionStateInput) {
53+
const {
54+
current_highest_bid,
55+
reserve_price,
56+
bid_increment,
57+
bid_increment_type,
58+
end_timestamp,
59+
current_timestamp = Math.floor(Date.now() / 1000)
60+
} = input;
61+
62+
const is_ended = current_timestamp >= end_timestamp;
63+
64+
let min_next_bid: number;
65+
66+
if (current_highest_bid === 0) {
67+
// No bids yet, next bid must be at least reserve price
68+
min_next_bid = reserve_price;
69+
} else {
70+
// Calculate increment
71+
const increment = bid_increment_type === 'percentage'
72+
? current_highest_bid * (bid_increment / 100)
73+
: bid_increment;
74+
75+
min_next_bid = current_highest_bid + increment;
76+
}
77+
78+
return {
79+
status: is_ended ? "ENDED" : "ACTIVE",
80+
current_highest_bid: current_highest_bid.toFixed(7),
81+
min_next_bid: min_next_bid.toFixed(7),
82+
reserve_price: reserve_price.toFixed(7),
83+
is_reserve_met: current_highest_bid >= reserve_price,
84+
time_remaining: Math.max(0, end_timestamp - current_timestamp),
85+
bid_increment_details: {
86+
type: bid_increment_type,
87+
value: bid_increment,
88+
}
89+
};
90+
}

‎tests/unit/auction_compute.test.ts‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { calculateDutchAuctionPrice, calculateEnglishAuctionState } from '../../src/tools/auction_compute.js';
3+
4+
describe('Auction Computations', () => {
5+
describe('calculateDutchAuctionPrice', () => {
6+
const start_timestamp = 1700000000;
7+
const end_timestamp = 1700010000; // 10000s duration
8+
const start_price = 1000;
9+
const reserve_price = 500;
10+
11+
it('returns start price before auction starts', async () => {
12+
const result = await calculateDutchAuctionPrice({
13+
start_price,
14+
reserve_price,
15+
start_timestamp,
16+
end_timestamp,
17+
current_timestamp: start_timestamp - 100,
18+
});
19+
20+
expect(result.status).toBe('UPCOMING');
21+
expect(result.current_price).toBe('1000.0000000');
22+
});
23+
24+
it('calculates price halfway through the auction', async () => {
25+
const result = await calculateDutchAuctionPrice({
26+
start_price,
27+
reserve_price,
28+
start_timestamp,
29+
end_timestamp,
30+
current_timestamp: start_timestamp + 5000,
31+
});
32+
33+
// 50% through: 1000 - 0.5 * (1000 - 500) = 1000 - 250 = 750
34+
expect(result.status).toBe('ACTIVE');
35+
expect(result.current_price).toBe('750.0000000');
36+
expect(result.progress_percentage).toBe('50.00');
37+
});
38+
39+
it('returns reserve price after auction ends', async () => {
40+
const result = await calculateDutchAuctionPrice({
41+
start_price,
42+
reserve_price,
43+
start_timestamp,
44+
end_timestamp,
45+
current_timestamp: end_timestamp + 100,
46+
});
47+
48+
expect(result.status).toBe('ENDED');
49+
expect(result.current_price).toBe('500.0000000');
50+
});
51+
});
52+
53+
describe('calculateEnglishAuctionState', () => {
54+
const end_timestamp = 1700000000;
55+
const reserve_price = 100;
56+
57+
it('calculates min bid when no bids exist', async () => {
58+
const result = await calculateEnglishAuctionState({
59+
current_highest_bid: 0,
60+
reserve_price,
61+
bid_increment: 10,
62+
bid_increment_type: 'absolute',
63+
end_timestamp,
64+
current_timestamp: end_timestamp - 100,
65+
});
66+
67+
expect(result.status).toBe('ACTIVE');
68+
expect(result.min_next_bid).toBe('100.0000000'); // Must beat reserve
69+
expect(result.is_reserve_met).toBe(false);
70+
});
71+
72+
it('calculates absolute bid increment', async () => {
73+
const result = await calculateEnglishAuctionState({
74+
current_highest_bid: 150,
75+
reserve_price,
76+
bid_increment: 10,
77+
bid_increment_type: 'absolute',
78+
end_timestamp,
79+
current_timestamp: end_timestamp - 100,
80+
});
81+
82+
expect(result.min_next_bid).toBe('160.0000000');
83+
expect(result.is_reserve_met).toBe(true);
84+
});
85+
86+
it('calculates percentage bid increment', async () => {
87+
const result = await calculateEnglishAuctionState({
88+
current_highest_bid: 200,
89+
reserve_price,
90+
bid_increment: 5, // 5%
91+
bid_increment_type: 'percentage',
92+
end_timestamp,
93+
current_timestamp: end_timestamp - 100,
94+
});
95+
96+
// 200 + 5% = 200 + 10 = 210
97+
expect(result.min_next_bid).toBe('210.0000000');
98+
});
99+
100+
it('handles ended auction state', async () => {
101+
const result = await calculateEnglishAuctionState({
102+
current_highest_bid: 250,
103+
reserve_price,
104+
bid_increment: 10,
105+
end_timestamp,
106+
current_timestamp: end_timestamp + 100,
107+
});
108+
109+
expect(result.status).toBe('ENDED');
110+
expect(result.time_remaining).toBe(0);
111+
});
112+
});
113+
});

0 commit comments

Comments
 (0)