Skip to content

Commit

Permalink
Min increment, targetAddress
Browse files Browse the repository at this point in the history
Fixes

Fixes

Fixes
  • Loading branch information
Boldizsar Mezei committed Oct 31, 2023
1 parent 166701a commit 2512d6f
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 45 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();
};
8 changes: 6 additions & 2 deletions packages/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ export const https = Object.entries(flattenObject(onRequests)).reduce(

// Trigger functions
const getFirestoreHandler = (config: TriggeredFunction) => {
const triggerConfig = {
document: config.document,
timeoutSeconds: config.runtimeOptions.timeoutSeconds,
};
if (config.type === TriggeredFunctionType.ON_CREATE) {
return functions.firestore.onDocumentCreated(config.document, async (event) => {
return functions.firestore.onDocumentCreated(triggerConfig, async (event) => {
const data = {
curr: event.data?.data(),
path: event.document,
Expand All @@ -35,7 +39,7 @@ const getFirestoreHandler = (config: TriggeredFunction) => {
config.type === TriggeredFunctionType.ON_UPDATE
? functions.firestore.onDocumentUpdated
: functions.firestore.onDocumentWritten;
return firestoreFunc(config.document, async (event) => {
return firestoreFunc(triggerConfig, async (event) => {
const data = {
prev: event.data?.before?.data(),
curr: event.data?.after?.data(),
Expand Down
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,6 +159,26 @@ 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);
Expand All @@ -169,7 +190,8 @@ const placeBid = (auction: Auction, order: string, bidder: string, amount: numbe
} 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 });
const invalidBid = { bidder, amount: Math.min(currentBid.amount, amount), order };
return { bids, invalidBid };
}
} else {
bids.push({ bidder, amount, order });
Expand All @@ -181,9 +203,3 @@ const placeBid = (auction: Auction, order: string, bidder: string, amount: numbe
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
Loading

0 comments on commit 2512d6f

Please sign in to comment.