make jobs expensive, priced based ranking rather than auction

This commit is contained in:
keyan 2022-03-03 12:56:02 -06:00
parent 026256c451
commit 1b84f76a27
8 changed files with 59 additions and 78 deletions

View File

@ -147,7 +147,7 @@ export default {
AND "pinId" IS NULL
${subClause(3)}
AND status = 'ACTIVE'
ORDER BY "maxBid" / 1000 DESC, created_at ASC
ORDER BY "maxBid" DESC, created_at ASC
OFFSET $2
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, sub)
break
@ -409,27 +409,15 @@ export default {
}
},
auctionPosition: async (parent, { id, sub, bid }, { models }) => {
// count items that have a bid gte to the current bid + 1000 or
// count items that have a bid gte to the current bid or
// gte current bid and older
const where = {
where: {
subName: sub,
status: 'ACTIVE',
OR: [{
maxBid: {
gte: bid + 1000
}
}, {
AND: [{
maxBid: {
gte: bid
}
}, {
createdAt: {
lt: new Date()
}
}]
}]
maxBid: {
gte: bid
}
}
}
@ -532,6 +520,10 @@ export default {
throw new UserInputError(`bid must be at least ${fullSub.baseCost}`, { argumentName: 'maxBid' })
}
if (maxBid % fullSub.deltaCost !== 0) {
throw new UserInputError(`bid must be a multiple of ${fullSub.deltaCost}`, { argumentName: 'maxBid' })
}
const checkSats = async () => {
// check if the user has the funds to run for the first minute
const minuteMsats = maxBid * 5 / 216

View File

@ -12,5 +12,6 @@ export default gql`
postTypes: [String!]!
rankingType: String!
baseCost: Int!
deltaCost: Int!
}
`

View File

@ -58,9 +58,9 @@ export default function JobForm ({ item, sub }) {
.or([Yup.string().email(), Yup.string().url()], 'invalid url or email')
.required('Required'),
maxBid: Yup.number('must be number')
.integer('must be integer').min(sub.baseCost, 'must be at least 10000')
.integer('must be integer').min(sub.baseCost, `must be at least ${sub.baseCost}`)
.max(100000000, 'must be less than 100000000')
.test('multiple', 'must be a multiple of 1000 sats', (val) => val % 1000 === 0)
.test('multiple', `must be a multiple of ${sub.deltaCost} sats`, (val) => val % sub.deltaCost === 0)
})
const position = data?.auctionPosition
@ -82,37 +82,10 @@ export default function JobForm ({ item, sub }) {
<ol className='font-weight-bold'>
<li>The higher your bid the higher your job will rank</li>
<li>The minimum bid is {sub.baseCost} sats/mo</li>
<li>You can increase or decrease your bid at anytime</li>
<li>You can edit or remove your job at anytime</li>
<li>Your sats/mo must be a multiple of {sub.deltaCost} sats</li>
<li>You can increase or decrease your bid, and edit or stop your job at anytime</li>
<li>Your job will be hidden if your wallet runs out of sats and can be unhidden by filling your wallet again</li>
</ol>
<AccordianItem
header={<div className='font-weight-bold'>How does ranking work in detail?</div>}
body={
<div>
<ol>
<li>You only pay as many sats/mo as required to maintain your position relative to other
posts (or {sub.baseCost} sats/mo) and only up to your max bid.
</li>
<li>Your sats/mo must be a multiple of 1000 sats</li>
</ol>
<div className='font-weight-bold text-muted'>By example</div>
<p>If your post's (A's) max bid is higher than another post (B) by at least
1000 sats/mo your post will rank higher and your wallet will pay 1000
sats/mo more than B.
</p>
<p>If another post (C) comes along whose max bid is higher than B's but less
than your's (A's), C will pay 1000 sats/mo more than B, and you will pay 1000 sats/mo
more than C.
</p>
<p>If a post (D) comes along whose max bid is higher than your's (A's), D
will pay 1000 stat/mo more than you (A), and the amount you (A) pays won't
change.
</p>
</div>
}
/>
</Modal.Body>
</Modal>
<Form
@ -171,7 +144,7 @@ export default function JobForm ({ item, sub }) {
/>
<Input
label={
<div className='d-flex align-items-center'>max bid
<div className='d-flex align-items-center'>bid
<Info width={18} height={18} className='fill-theme-color pointer ml-1' onClick={() => setInfo(true)} />
</div>
}
@ -185,7 +158,7 @@ export default function JobForm ({ item, sub }) {
}
}}
append={<InputGroup.Text className='text-monospace'>sats/month</InputGroup.Text>}
hint={<span className='text-muted'>up to {pull} sats/min will be pulled from your wallet</span>}
hint={<span className='text-muted'>{pull} sats/min will be pulled from your wallet</span>}
/>
<div className='font-weight-bold text-muted'>This bid puts your job in position: {position}</div>
{item && <StatusControl item={item} />}

View File

@ -21,6 +21,7 @@ export const ITEM_FIELDS = gql`
sub {
name
baseCost
deltaCost
}
status
mine

View File

@ -7,6 +7,7 @@ export const SUB_FIELDS = gql`
postTypes
rankingType
baseCost
deltaCost
}`
export const SUB = gql`

View File

@ -0,0 +1,35 @@
-- AlterTable
ALTER TABLE "Sub" ADD COLUMN "deltaCost" INTEGER NOT NULL DEFAULT 0;
-- charge the user for the auction item
CREATE OR REPLACE FUNCTION run_auction(item_id INTEGER) RETURNS void AS $$
DECLARE
bid INTEGER;
user_id INTEGER;
user_msats INTEGER;
item_status "Status";
BEGIN
PERFORM ASSERT_SERIALIZED();
-- extract data we need
SELECT ("maxBid" * 5 / 216), "userId", status INTO bid, user_id, item_status FROM "Item" WHERE id = item_id;
SELECT msats INTO user_msats FROM users WHERE id = user_id;
-- check if user wallet has enough sats
IF bid > user_msats THEN
-- if not, set status = NOSATS and statusUpdatedAt to now_utc if not already set
IF item_status <> 'NOSATS' THEN
UPDATE "Item" SET status = 'NOSATS', "statusUpdatedAt" = now_utc() WHERE id = item_id;
END IF;
ELSE
-- if so, deduct from user
UPDATE users SET msats = msats - bid WHERE id = user_id;
-- update item status = ACTIVE and statusUpdatedAt = null if NOSATS
IF item_status = 'NOSATS' THEN
UPDATE "Item" SET status = 'ACTIVE', "statusUpdatedAt" = now_utc() WHERE id = item_id;
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;
UPDATE "Sub" SET "baseCost" = 500000, "deltaCost" = 50000 WHERE name ='jobs';

View File

@ -145,6 +145,7 @@ model Sub {
postTypes PostType[]
rankingType RankingType
baseCost Int @default(1)
deltaCost Int @default(0)
desc String?
Item Item[]

View File

@ -3,12 +3,7 @@ const serialize = require('../api/resolvers/serial')
function auction ({ models }) {
return async function ({ name }) {
console.log('running', name)
// TODO: do this for each sub with auction ranking
// because we only have one auction sub, we don't need to do this
const SUB_BASE_COST = 10000
const BID_DELTA = 1000
// get all items we need to check in order of low to high bid
// get all items we need to check
const items = await models.item.findMany(
{
where: {
@ -18,33 +13,15 @@ function auction ({ models }) {
status: {
not: 'STOPPED'
}
},
orderBy: {
maxBid: 'asc'
}
}
)
// we subtract bid delta so that the lowest bidder, pays
// the sub base cost
let lastBid = SUB_BASE_COST - BID_DELTA
// for each item, run serialized auction function
for (const item of items) {
let bid = lastBid
// if this item's maxBid is great enough, have them pay more
// else have them match the last bid
if (item.maxBid >= lastBid + BID_DELTA) {
bid = lastBid + BID_DELTA
}
const [{ run_auction: succeeded }] = await serialize(models,
models.$queryRaw`SELECT run_auction(${item.id}, ${bid})`)
// if we succeeded update the lastBid
if (succeeded) {
lastBid = bid
}
}
items.forEach(async item => {
await serialize(models,
models.$executeRaw`SELECT run_auction(${item.id})`)
})
console.log('done', name)
}