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

View File

@ -12,5 +12,6 @@ export default gql`
postTypes: [String!]! postTypes: [String!]!
rankingType: String! rankingType: String!
baseCost: Int! 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') .or([Yup.string().email(), Yup.string().url()], 'invalid url or email')
.required('Required'), .required('Required'),
maxBid: Yup.number('must be number') 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') .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 const position = data?.auctionPosition
@ -82,37 +82,10 @@ export default function JobForm ({ item, sub }) {
<ol className='font-weight-bold'> <ol className='font-weight-bold'>
<li>The higher your bid the higher your job will rank</li> <li>The higher your bid the higher your job will rank</li>
<li>The minimum bid is {sub.baseCost} sats/mo</li> <li>The minimum bid is {sub.baseCost} sats/mo</li>
<li>You can increase or decrease your bid at anytime</li> <li>Your sats/mo must be a multiple of {sub.deltaCost} sats</li>
<li>You can edit or remove your job at anytime</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> <li>Your job will be hidden if your wallet runs out of sats and can be unhidden by filling your wallet again</li>
</ol> </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.Body>
</Modal> </Modal>
<Form <Form
@ -171,7 +144,7 @@ export default function JobForm ({ item, sub }) {
/> />
<Input <Input
label={ 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)} /> <Info width={18} height={18} className='fill-theme-color pointer ml-1' onClick={() => setInfo(true)} />
</div> </div>
} }
@ -185,7 +158,7 @@ export default function JobForm ({ item, sub }) {
} }
}} }}
append={<InputGroup.Text className='text-monospace'>sats/month</InputGroup.Text>} 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> <div className='font-weight-bold text-muted'>This bid puts your job in position: {position}</div>
{item && <StatusControl item={item} />} {item && <StatusControl item={item} />}

View File

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

View File

@ -7,6 +7,7 @@ export const SUB_FIELDS = gql`
postTypes postTypes
rankingType rankingType
baseCost baseCost
deltaCost
}` }`
export const SUB = gql` 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[] postTypes PostType[]
rankingType RankingType rankingType RankingType
baseCost Int @default(1) baseCost Int @default(1)
deltaCost Int @default(0)
desc String? desc String?
Item Item[] Item Item[]

View File

@ -3,12 +3,7 @@ const serialize = require('../api/resolvers/serial')
function auction ({ models }) { function auction ({ models }) {
return async function ({ name }) { return async function ({ name }) {
console.log('running', name) console.log('running', name)
// TODO: do this for each sub with auction ranking // get all items we need to check
// 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
const items = await models.item.findMany( const items = await models.item.findMany(
{ {
where: { where: {
@ -18,33 +13,15 @@ function auction ({ models }) {
status: { status: {
not: 'STOPPED' 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 each item, run serialized auction function
for (const item of items) { items.forEach(async item => {
let bid = lastBid await serialize(models,
// if this item's maxBid is great enough, have them pay more models.$executeRaw`SELECT run_auction(${item.id})`)
// 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
}
}
console.log('done', name) console.log('done', name)
} }