Skip to content

Commit

Permalink
Min increment, targetAddress
Browse files Browse the repository at this point in the history
  • Loading branch information
Boldizsar Mezei committed Oct 31, 2023
1 parent 166701a commit aed1800
Show file tree
Hide file tree
Showing 17 changed files with 334 additions and 36 deletions.
2 changes: 2 additions & 0 deletions packages/functions/scripts/dbUpgrades/1.0.0/auction.roll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ export const nftAuctionRoll = async (app: FirebaseApp) => {
const getAuctionData = async (nft: Nft) => {
const auction: Auction = {
uid: getRandomEthAddress(),
space: nft.space,
createdBy: nft.owner,
project: nft.owner || SOON_PROJECT_ID,
projects: nft.projects || { [SOON_PROJECT_ID]: true },
auctionFrom: nft.auctionFrom!,
auctionTo: nft.auctionTo!,
auctionFloorPrice: nft.auctionFloorPrice || 0,
minimalBidIncrement: 0,
auctionLength: nft.auctionLength || 0,

bids: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import {
} from '@build-5/interfaces';
import dayjs from 'dayjs';
import Joi from 'joi';
import { toJoiObject } from '../../services/joi/common';
import { CommonJoi, toJoiObject } from '../../services/joi/common';
import { AVAILABLE_NETWORKS } from '../common';

const minAvailableFrom = 10;
const minBids = 1;
const maxBids = 10;

export const auctionCreateSchema = {
space: CommonJoi.uid().description('Build5 id of the space'),
auctionFrom: Joi.date()
.greater(dayjs().subtract(minAvailableFrom, 'minutes').toDate())
.required()
Expand All @@ -29,6 +30,13 @@ export const auctionCreateSchema = {
.description(
`Floor price of the auction. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`,
),
minimalBidIncrement: Joi.number()
.min(MIN_IOTA_AMOUNT)
.max(MAX_IOTA_AMOUNT)
.optional()
.description(
`Defines the minimum increment of a subsequent bid. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`,
),
auctionLength: Joi.number()
.min(TRANSACTION_AUTO_EXPIRY_MS)
.max(TRANSACTION_MAX_EXPIRY_MS)
Expand Down Expand Up @@ -65,6 +73,7 @@ export const auctionCreateSchema = {
topUpBased: Joi.boolean().description(
'If set to true, consequent bids from the same user will be treated as topups',
),
targetAddress: Joi.string().description('A valid network address where funds will be sent.'),
};

export const auctionCreateSchemaObject = toJoiObject<AuctionCreateRequest>(auctionCreateSchema)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { build5Db } from '@build-5/database';
import { Auction, AuctionCreateRequest, COL, Member, WenError } from '@build-5/interfaces';
import { getAuctionData } from '../../services/payment/tangle-service/auction/auction.create.service';
import { invalidArgument } from '../../utils/error.utils';
import { assertIsSpaceMember } from '../../utils/space.utils';
import { Context } from '../common';

export const auctionCreateControl = async ({
Expand All @@ -15,7 +16,9 @@ export const auctionCreateControl = async ({
throw invalidArgument(WenError.member_does_not_exists);
}

const auction = getAuctionData(project, owner, params);
await assertIsSpaceMember(params.space, owner);

const auction = getAuctionData(project, member, params);
const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`);
await auctionDocRef.create(auction);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export const baseNftSetForSaleSchema = {
.min(MIN_IOTA_AMOUNT)
.max(MAX_IOTA_AMOUNT)
.description(`Floor price of the nft. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`),
minimalBidIncrement: Joi.number()
.min(MIN_IOTA_AMOUNT)
.max(MAX_IOTA_AMOUNT)
.optional()
.description(
`Defines the minimum increment of a subsequent bid. Minimum ${MIN_IOTA_AMOUNT}, maximum ${MAX_IOTA_AMOUNT}`,
),
auctionLength: Joi.number()
.min(TRANSACTION_AUTO_EXPIRY_MS)
.max(TRANSACTION_MAX_EXPIRY_MS)
Expand Down
65 changes: 61 additions & 4 deletions packages/functions/src/cron/auction.cron.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { build5Db } from '@build-5/database';
import { Auction, AuctionType, COL } from '@build-5/interfaces';
import {
Auction,
AuctionType,
COL,
Member,
Transaction,
TransactionType,
} from '@build-5/interfaces';
import dayjs from 'dayjs';
import { AuctionFinalizeService } from '../services/payment/auction/auction.finalize.service';
import { TransactionService } from '../services/payment/transaction-service';
import { getAddress } from '../utils/address.utils';
import { getProject, getProjects } from '../utils/common.utils';
import { getRandomEthAddress } from '../utils/wallet.utils';

export const finalizeAuctions = async () => {
const snap = await build5Db()
.collection(COL.AUCTION)
.where('auctionTo', '<=', dayjs().toDate())
.where('active', '==', true)
.get<Auction>();
const promises = snap.map(async (a) => {
if (a.type === AuctionType.NFT) {
await finalizeNftAuction(a.uid);
const promises = snap.map((a) => {
switch (a.type) {
case AuctionType.NFT:
return finalizeNftAuction(a.uid);
case AuctionType.OPEN:
return finalizeOpenAuction(a);
}
});
await Promise.all(promises);
Expand All @@ -25,3 +38,47 @@ const finalizeNftAuction = (auction: string) =>
await service.markAsFinalized(auction);
tranService.submit();
});

const finalizeOpenAuction = async (auction: Auction) => {
const batch = build5Db().batch();

let targetAddress = auction.targetAddress;
if (!targetAddress) {
const memberDocRef = build5Db().doc(`${COL.MEMBER}/${auction.createdBy}`);
const member = <Member>await memberDocRef.get();
targetAddress = getAddress(member, auction.network);
}

const payments = await build5Db()
.collection(COL.TRANSACTION)
.where('type', '==', TransactionType.PAYMENT)
.where('payload.invalidPayment', '==', false)
.where('payload.auction', '==', auction.uid)
.get<Transaction>();

for (const payment of payments) {
const billPayment: Transaction = {
project: getProject(payment),
projects: getProjects([payment]),
type: TransactionType.BILL_PAYMENT,
uid: getRandomEthAddress(),
space: auction.space,
member: payment.member,
network: payment.network,
payload: {
amount: payment.payload.amount!,
sourceAddress: payment.payload.targetAddress,
targetAddress,
sourceTransaction: [payment.uid],
reconciled: true,
royalty: false,
void: false,
auction: auction.uid,
},
};
const billPaymentDocRef = build5Db().doc(`${COL.TRANSACTION}/${billPayment.uid}`);
batch.create(billPaymentDocRef, billPayment);
}

await batch.commit();
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
TransactionType,
} from '@build-5/interfaces';
import dayjs from 'dayjs';
import { head, last, set } from 'lodash';
import { head, set } from 'lodash';
import { NotificationService } from '../../notification/notification';
import { HandlerParams } from '../base';
import { TransactionService } from '../transaction-service';
Expand All @@ -35,6 +35,7 @@ export class AuctionBidService {
}

this.transactionService.markAsReconciled(order, match.msgId);

const payment = await this.transactionService.createPayment(order, match);
await this.addNewBid(owner, auction, order, payment);
};
Expand All @@ -45,13 +46,12 @@ export class AuctionBidService {
order: Transaction,
payment: Transaction,
): Promise<void> => {
if (paidAmountIsBelowFloor(payment, auction) || newPaymentTooLow(payment, auction)) {
if (!isValidBid(payment, auction)) {
await this.creditAsInvalidPayment(payment);
return;
}

const { bids, invalidBid } = placeBid(auction, order.uid, owner, payment.payload.amount!);

const auctionUpdateData = this.getAuctionUpdateData(auction, bids);

if (invalidBid) {
Expand Down Expand Up @@ -99,6 +99,7 @@ export class AuctionBidService {
action: 'update',
});
const paymentPayload = payment.payload;
set(payment, 'payload.invalidPayment', true);
await this.transactionService.createCredit(TransactionPayloadType.INVALID_PAYMENT, payment, {
msgId: paymentPayload.chainReference!,
to: {
Expand Down Expand Up @@ -158,32 +159,39 @@ export class AuctionBidService {
};
}

const isValidBid = (payment: Transaction, auction: Auction) => {
const amount = payment.payload.amount!;
const prevBid = auction.bids.find((b) => b.bidder === payment.member);
const prevBidAmount = prevBid?.amount || 0;

if (auction.topUpBased) {
return (
prevBidAmount + amount >= auction.auctionFloorPrice &&
amount >= auction.minimalBidIncrement &&
(prevBid !== undefined || amount > (auction.bids[auction.maxBids - 1]?.amount || 0))
);
}

return (
amount > (auction.auctionHighestBid || 0) &&
amount >= auction.auctionFloorPrice &&
amount - prevBidAmount >= auction.minimalBidIncrement
);
};

const placeBid = (auction: Auction, order: string, bidder: string, amount: number) => {
const bids = [...auction.bids];
const currentBid = bids.find((b) => b.bidder === bidder);

if (currentBid) {
if (auction.topUpBased) {
currentBid.amount += amount;
bids.sort((a, b) => b.amount - a.amount);
} else {
currentBid.amount = Math.max(currentBid.amount, amount);
bids.sort((a, b) => b.amount - a.amount);
bids.push({ bidder, amount: Math.min(currentBid.amount, amount), order });
}
if (currentBid && auction.topUpBased) {
currentBid.amount += amount;
} else {
bids.push({ bidder, amount, order });
bids.sort((a, b) => b.amount - a.amount);
}
bids.sort((a, b) => b.amount - a.amount);

return {
bids: bids.slice(0, auction.maxBids),
invalidBid: head(bids.slice(auction.maxBids)),
};
};

const paidAmountIsBelowFloor = (payment: Transaction, auction: Auction) =>
payment.payload.amount! < auction.auctionFloorPrice;

const newPaymentTooLow = (payment: Transaction, auction: Auction) =>
!auction.topUpBased && (last(auction.bids)?.amount || 0) > payment.payload.amount!;
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {
AuctionCreateTangleRequest,
AuctionType,
COL,
Member,
Network,
} from '@build-5/interfaces';
import dayjs from 'dayjs';
import { assertMemberHasValidAddress, getAddress } from '../../../../utils/address.utils';
import { getProjects } from '../../../../utils/common.utils';
import { dateToTimestamp } from '../../../../utils/dateTime.utils';
import { assertValidationAsync } from '../../../../utils/schema.utils';
Expand All @@ -22,7 +24,10 @@ export class TangleAuctionCreateService {
public handleRequest = async ({ request, project, owner }: HandlerParams) => {
const params = await assertValidationAsync(auctionCreateTangleSchema, request);

const auction = getAuctionData(project, owner, params);
const memberDocRef = build5Db().doc(`${COL.MEMBER}/${owner}`);
const member = <Member>await memberDocRef.get();

const auction = getAuctionData(project, member, params);
const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auction.uid}`);

this.transactionService.push({ ref: auctionDocRef, data: auction, action: 'set' });
Expand All @@ -33,19 +38,27 @@ export class TangleAuctionCreateService {

export const getAuctionData = (
project: string,
owner: string,
member: Member,
params: AuctionCreateRequest | AuctionCreateTangleRequest,
) => {
let targetAddress = params.targetAddress;
if (!targetAddress) {
assertMemberHasValidAddress(member, params.network as Network);
targetAddress = getAddress(member, params.network as Network);
}

const auction: Auction = {
uid: getRandomEthAddress(),
space: params.space,
project,
projects: getProjects([], project),
createdBy: owner,
createdBy: member.uid,
auctionFrom: dateToTimestamp(params.auctionFrom),
auctionTo: dateToTimestamp(dayjs(params.auctionFrom).add(params.auctionLength)),
auctionLength: params.auctionLength,

auctionFloorPrice: params.auctionFloorPrice || 0,
minimalBidIncrement: params.minimalBidIncrement || 0,

maxBids: params.maxBids,

Expand All @@ -56,6 +69,8 @@ export const getAuctionData = (
topUpBased: params.topUpBased || false,

bids: [],

targetAddress,
};

if (params.extendedAuctionLength && params.extendAuctionWithin) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,17 @@ const getAuctionData = (project: string, owner: string, params: NftSetForSaleReq
}
const auction: Auction = {
uid: getRandomEthAddress(),
space: nft.space,
createdBy: owner,
project,
projects: getProjects([], project),
auctionFrom: dateToTimestamp(params.auctionFrom),
auctionTo: dateToTimestamp(dayjs(params.auctionFrom).add(params.auctionLength || 0)),
auctionFloorPrice: params.auctionFloorPrice || 0,
auctionLength: params.auctionLength!,

auctionFloorPrice: params.auctionFloorPrice || 0,
minimalBidIncrement: params.minimalBidIncrement || 0,

bids: [],
maxBids: 1,
type: AuctionType.NFT,
Expand Down
18 changes: 14 additions & 4 deletions packages/functions/test/controls/auction/Helper.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { IDocument, build5Db } from '@build-5/database';
import { Auction, COL, MIN_IOTA_AMOUNT, Network } from '@build-5/interfaces';
import { Auction, COL, MIN_IOTA_AMOUNT, Network, Space } from '@build-5/interfaces';
import dayjs from 'dayjs';
import { auctionCreate, bidAuction } from '../../../src/runtime/firebase/auction/index';
import * as wallet from '../../../src/utils/wallet.utils';
import { testEnv } from '../../set-up';
import { createMember, mockWalletReturnValue, submitMilestoneFunc } from '../common';
import { createMember, createSpace, mockWalletReturnValue, submitMilestoneFunc } from '../common';

export class Helper {
public spy: any = {} as any;
public space: Space = {} as any;
public member: string = {} as any;
public members: string[] = [];
public auction: Auction = {} as any;
Expand All @@ -19,10 +20,18 @@ export class Helper {

public beforeEach = async (now: dayjs.Dayjs) => {
this.member = await createMember(this.spy);
this.space = await createSpace(this.spy, this.member);
const memberPromises = Array.from(Array(3)).map(() => createMember(this.spy));
this.members = await Promise.all(memberPromises);

mockWalletReturnValue(this.spy, this.member, auctionRequest(now));
await this.createAuction(now);
};

public createAuction = async (now: dayjs.Dayjs, customAuctionParams?: { [key: string]: any }) => {
mockWalletReturnValue(this.spy, this.member, {
...auctionRequest(this.space.uid, now),
...customAuctionParams,
});
this.auction = await testEnv.wrap(auctionCreate)({});
this.auctionDocRef = build5Db().doc(`${COL.AUCTION}/${this.auction.uid}`);
};
Expand All @@ -35,7 +44,8 @@ export class Helper {
};
}

const auctionRequest = (now: dayjs.Dayjs, auctionLength = 60000 * 4) => ({
const auctionRequest = (space: string, now: dayjs.Dayjs, auctionLength = 60000 * 4) => ({
space,
auctionFrom: now.toDate(),
auctionFloorPrice: 2 * MIN_IOTA_AMOUNT,
auctionLength,
Expand Down
Loading

0 comments on commit aed1800

Please sign in to comment.