make jobs expensive, priced based ranking rather than auction
This commit is contained in:
parent
026256c451
commit
1b84f76a27
|
@ -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
|
||||
|
|
|
@ -12,5 +12,6 @@ export default gql`
|
|||
postTypes: [String!]!
|
||||
rankingType: String!
|
||||
baseCost: Int!
|
||||
deltaCost: Int!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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} />}
|
||||
|
|
|
@ -21,6 +21,7 @@ export const ITEM_FIELDS = gql`
|
|||
sub {
|
||||
name
|
||||
baseCost
|
||||
deltaCost
|
||||
}
|
||||
status
|
||||
mine
|
||||
|
|
|
@ -7,6 +7,7 @@ export const SUB_FIELDS = gql`
|
|||
postTypes
|
||||
rankingType
|
||||
baseCost
|
||||
deltaCost
|
||||
}`
|
||||
|
||||
export const SUB = gql`
|
||||
|
|
|
@ -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';
|
|
@ -145,6 +145,7 @@ model Sub {
|
|||
postTypes PostType[]
|
||||
rankingType RankingType
|
||||
baseCost Int @default(1)
|
||||
deltaCost Int @default(0)
|
||||
desc String?
|
||||
|
||||
Item Item[]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue