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
|
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
|
||||||
|
|
|
@ -12,5 +12,6 @@ export default gql`
|
||||||
postTypes: [String!]!
|
postTypes: [String!]!
|
||||||
rankingType: String!
|
rankingType: String!
|
||||||
baseCost: Int!
|
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')
|
.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} />}
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const ITEM_FIELDS = gql`
|
||||||
sub {
|
sub {
|
||||||
name
|
name
|
||||||
baseCost
|
baseCost
|
||||||
|
deltaCost
|
||||||
}
|
}
|
||||||
status
|
status
|
||||||
mine
|
mine
|
||||||
|
|
|
@ -7,6 +7,7 @@ export const SUB_FIELDS = gql`
|
||||||
postTypes
|
postTypes
|
||||||
rankingType
|
rankingType
|
||||||
baseCost
|
baseCost
|
||||||
|
deltaCost
|
||||||
}`
|
}`
|
||||||
|
|
||||||
export const SUB = gql`
|
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[]
|
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[]
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue