Skip to content

Commit

Permalink
Merge pull request #2762 from build-5/develop
Browse files Browse the repository at this point in the history
Merge bulk buy
  • Loading branch information
adamunchained authored Jan 17, 2024
2 parents 9c60bcf + 9d60f7d commit 0cfda60
Show file tree
Hide file tree
Showing 46 changed files with 1,861 additions and 178 deletions.
196 changes: 135 additions & 61 deletions .github/workflows/functions_tangle-online-unit-tests_emulator.yml

Large diffs are not rendered by default.

208 changes: 141 additions & 67 deletions .github/workflows/functions_tangle-unit-tests.yml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MAX_NFT_BULK_PURCHASE, NftPurchaseBulkRequest } from '@build-5/interfaces';
import Joi from 'joi';
import { toJoiObject } from '../../services/joi/common';
import { nftPurchaseSchema } from './NftPurchaseRequestSchema';

export const nftPurchaseBulkSchema = toJoiObject<NftPurchaseBulkRequest>({
orders: Joi.array()
.items(nftPurchaseSchema)
.min(1)
.max(MAX_NFT_BULK_PURCHASE)
.description(
`List of collections&nfts to purchase, minimum 1, maximum ${MAX_NFT_BULK_PURCHASE}`,
)
.required(),
})
.description('Request object to create an NFT bulk purchase order')
.meta({
className: 'NftPurchaseBulkRequest',
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const nftPurchaseSchema = toJoiObject<NftPurchaseRequest>({
collection: CommonJoi.uid().description(
'Build5 id of the collection in case a random nft is bought.',
),
nft: CommonJoi.uid(false).description('Build5 if of the nft to be purchased.'),
nft: CommonJoi.uid(false).description('Build5 id of the nft to be purchased.'),
})
.description('Request object to create an NFT purchase order')
.meta({
Expand Down
17 changes: 17 additions & 0 deletions packages/functions/src/controls/nft/nft.puchase.bulk.control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { build5Db } from '@build-5/database';
import { COL, NftPurchaseBulkRequest, Transaction } from '@build-5/interfaces';
import { createNftBulkOrder } from '../../services/payment/tangle-service/nft/nft-purchase.bulk.service';
import { Context } from '../common';

export const orderNftBulkControl = async ({
ip,
owner,
params,
project,
}: Context<NftPurchaseBulkRequest>): Promise<Transaction> => {
const order = await createNftBulkOrder(project, params.orders, owner, ip);
const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${order.uid}`);
await orderDocRef.create(order);

return (await orderDocRef.get<Transaction>())!;
};
2 changes: 2 additions & 0 deletions packages/functions/src/runtime/firebase/nft/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const depositNft = https[WEN_FUNC.depositNft];

export const orderNft = https[WEN_FUNC.orderNft];

export const orderNftBulk = https[WEN_FUNC.orderNftBulk];

export const stakeNft = https[WEN_FUNC.stakeNft];

export const openBid = https[WEN_FUNC.openBid];
8 changes: 8 additions & 0 deletions packages/functions/src/runtime/https/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { updateMemberControl } from '../../controls/member/member.update';
import { nftBidSchema } from '../../controls/nft/NftBidRequestSchema';
import { createSchema, nftCreateSchema } from '../../controls/nft/NftCreateRequestSchema';
import { depositNftSchema } from '../../controls/nft/NftDepositRequestSchema';
import { nftPurchaseBulkSchema } from '../../controls/nft/NftPurchaseBulkRequestSchema';
import { nftPurchaseSchema } from '../../controls/nft/NftPurchaseRequestSchema';
import { setNftForSaleSchema } from '../../controls/nft/NftSetForSaleRequestSchema';
import { stakeNftSchema } from '../../controls/nft/NftStakeRequestSchema';
Expand All @@ -45,6 +46,7 @@ import { nftWithdrawSchema } from '../../controls/nft/NftWithdrawRequestSchema';
import { nftBidControl } from '../../controls/nft/nft.bid.control';
import { createBatchNftControl, createNftControl } from '../../controls/nft/nft.create';
import { depositNftControl } from '../../controls/nft/nft.deposit';
import { orderNftBulkControl } from '../../controls/nft/nft.puchase.bulk.control';
import { orderNftControl } from '../../controls/nft/nft.puchase.control';
import { setForSaleNftControl } from '../../controls/nft/nft.set.for.sale';
import { nftStakeControl } from '../../controls/nft/nft.stake';
Expand Down Expand Up @@ -344,6 +346,12 @@ exports[WEN_FUNC.orderNft] = onRequest({
handler: orderNftControl,
});

exports[WEN_FUNC.orderNftBulk] = onRequest({
name: WEN_FUNC.orderNftBulk,
schema: nftPurchaseBulkSchema,
handler: orderNftBulkControl,
});

exports[WEN_FUNC.openBid] = onRequest({
name: WEN_FUNC.openBid,
schema: nftBidSchema,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { build5Db } from '@build-5/database';
import {
COL,
Collection,
Entity,
Nft,
NftBulkOrder,
SUB_COL,
Space,
TRANSACTION_AUTO_EXPIRY_MS,
Transaction,
TransactionPayloadType,
TransactionType,
TransactionValidationType,
getMilestoneCol,
} from '@build-5/interfaces';
import dayjs from 'dayjs';
import { get } from 'lodash';
import { getAddress } from '../../../utils/address.utils';
import { getRestrictions } from '../../../utils/common.utils';
import { dateToTimestamp } from '../../../utils/dateTime.utils';
import { getSpace } from '../../../utils/space.utils';
import { getRandomEthAddress } from '../../../utils/wallet.utils';
import { WalletService } from '../../wallet/wallet.service';
import { BaseService, HandlerParams } from '../base';
import { assertNftCanBePurchased, getMember } from '../tangle-service/nft/nft-purchase.service';
import { NftPurchaseService } from './nft-purchase.service';

export class NftPurchaseBulkService extends BaseService {
public handleRequest = async ({ order, match, tranEntry, tran, project }: HandlerParams) => {
const payment = await this.transactionService.createPayment(order, match);

const promises = (order.payload.nftOrders || []).map((nftOrder) =>
this.createNftPurchaseOrder(project, order, nftOrder),
);
const nftOrders = await Promise.all(promises);

const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${order.uid}`);
this.transactionService.push({
ref: orderDocRef,
data: {
'payload.nftOrders': nftOrders,
'payload.reconciled': true,
'payload.chainReference': match.msgId,
},
action: 'update',
});

const total = nftOrders.reduce((acc, act) => acc + act.price, 0);
if (total < tranEntry.amount) {
const credit = {
project,
type: TransactionType.CREDIT,
uid: getRandomEthAddress(),
space: order.space,
member: order.member || match.from,
network: order.network,
payload: {
type: TransactionPayloadType.NFT_PURCHASE_BULK,
amount: tranEntry.amount - total,
sourceAddress: order.payload.targetAddress,
targetAddress: match.from,
sourceTransaction: [payment.uid],
reconciled: false,
void: false,
},
};
const docRef = build5Db().doc(`${COL.TRANSACTION}/${credit.uid}`);
this.transactionService.push({ ref: docRef, data: credit, action: 'set' });
}

if (total) {
const targetAddresses = nftOrders
.filter((o) => o.price > 0)
.map((o) => ({ toAddress: o.targetAddress!, amount: o.price }));
const transfer: Transaction = {
project,
type: TransactionType.UNLOCK,
uid: getRandomEthAddress(),
space: order.space || '',
member: order.member || match.from,
network: order.network,
payload: {
type: TransactionPayloadType.TANGLE_TRANSFER_MANY,
amount: total,
sourceAddress: order.payload.targetAddress,
targetAddresses,
sourceTransaction: [payment.uid],
expiresOn: dateToTimestamp(dayjs().add(TRANSACTION_AUTO_EXPIRY_MS)),
milestoneTransactionPath: `${getMilestoneCol(order.network!)}/${tran.milestone}/${
SUB_COL.TRANSACTIONS
}/${tran.uid}`,
},
};
const docRef = build5Db().doc(`${COL.TRANSACTION}/${transfer.uid}`);
this.transactionService.push({ ref: docRef, data: transfer, action: 'set' });
}
};

private createNftPurchaseOrder = async (
project: string,
order: Transaction,
nftOrder: NftBulkOrder,
) => {
if (!nftOrder.price) {
return { ...nftOrder, targetAddress: '' };
}

const nftDocRef = build5Db().doc(`${COL.NFT}/${nftOrder.nft}`);
const nft = <Nft>await this.transactionService.get(nftDocRef);

const collectionDocRef = build5Db().doc(`${COL.COLLECTION}/${nft.collection}`);
const collection = <Collection>await collectionDocRef.get();

const spaceDocRef = build5Db().doc(`${COL.SPACE}/${nft.space}`);
const space = <Space>await spaceDocRef.get();
try {
await assertNftCanBePurchased(
space,
collection,
nft,
nftOrder.requestedNft,
order.member!,
true,
);

if (nft.auction) {
const service = new NftPurchaseService(this.transactionService);
await service.creditBids(nft.auction);
}

const wallet = await WalletService.newWallet(order.network);
const targetAddress = await wallet.getNewIotaAddressDetails();

const royaltySpace = await getSpace(collection.royaltiesSpace);

const nftPurchaseOrderId = getRandomEthAddress();

const nftDocRef = build5Db().doc(`${COL.NFT}/${nft.uid}`);
this.transactionService.push({
ref: nftDocRef,
data: { locked: true, lockedBy: order.uid },
action: 'update',
});

const currentOwner = nft.owner ? await getMember(nft.owner) : space;

const nftPurchaseOrder = {
project,
type: TransactionType.ORDER,
uid: nftPurchaseOrderId,
member: order.member!,
space: space.uid,
network: order.network,
payload: {
type: TransactionPayloadType.NFT_PURCHASE,
amount: nftOrder.price,
targetAddress: targetAddress.bech32,
beneficiary: nft.owner ? Entity.MEMBER : Entity.SPACE,
beneficiaryUid: nft.owner || collection.space,
beneficiaryAddress: getAddress(currentOwner, order.network),
royaltiesFee: collection.royaltiesFee,
royaltiesSpace: collection.royaltiesSpace || '',
royaltiesSpaceAddress: getAddress(royaltySpace, order.network),
expiresOn: dateToTimestamp(dayjs().add(TRANSACTION_AUTO_EXPIRY_MS)),
validationType: TransactionValidationType.ADDRESS_AND_AMOUNT,
reconciled: false,
void: false,
chainReference: null,
nft: nft.uid,
collection: collection.uid,
restrictions: getRestrictions(collection, nft),
},
linkedTransactions: [],
};
const docRef = build5Db().doc(`${COL.TRANSACTION}/${nftPurchaseOrder.uid}`);
this.transactionService.push({ ref: docRef, data: nftPurchaseOrder, action: 'set' });

return { ...nftOrder, targetAddress: targetAddress.bech32 };
} catch (error) {
return {
...nftOrder,
price: 0,
error: get(error, 'details.code', 0),
targetAddress: '',
} as NftBulkOrder;
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class NftPurchaseService extends BaseService {
}
};

private creditBids = async (auctionId: string) => {
public creditBids = async (auctionId: string) => {
const auctionDocRef = build5Db().doc(`${COL.AUCTION}/${auctionId}`);
const auction = <Auction>await this.transaction.get(auctionDocRef);
this.transactionService.push({
Expand Down
3 changes: 3 additions & 0 deletions packages/functions/src/services/payment/payment-processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { CreditService } from './credit-service';
import { MetadataNftService } from './metadataNft-service';
import { CollectionMintingService } from './nft/collection-minting.service';
import { NftDepositService } from './nft/nft-deposit.service';
import { NftPurchaseBulkService } from './nft/nft-purchase.bulk.service';
import { NftPurchaseService } from './nft/nft-purchase.service';
import { NftStakeService } from './nft/nft-stake.service';
import { SpaceClaimService } from './space/space-service';
Expand Down Expand Up @@ -143,6 +144,8 @@ export class ProcessingService {
switch (type) {
case TransactionPayloadType.NFT_PURCHASE:
return new NftPurchaseService(tranService);
case TransactionPayloadType.NFT_PURCHASE_BULK:
return new NftPurchaseBulkService(tranService);
case TransactionPayloadType.NFT_BID:
case TransactionPayloadType.AUCTION_BID:
return new AuctionBidService(tranService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AwardCreateService } from './award/award.create.service';
import { AwardFundService } from './award/award.fund.service';
import { MintMetadataNftService } from './metadataNft/mint-metadata-nft.service';
import { NftDepositService } from './nft/nft-deposit.service';
import { TangleNftPurchaseBulkService } from './nft/nft-purchase.bulk.service';
import { TangleNftPurchaseService } from './nft/nft-purchase.service';
import { TangleNftSetForSaleService } from './nft/nft-set-for-sale.service';
import { ProposalApprovalService } from './proposal/ProposalApporvalService';
Expand Down Expand Up @@ -96,6 +97,8 @@ export class TangleRequestService extends BaseTangleService<TangleResponse> {
return new TangleStakeService(this.transactionService);
case TangleRequestType.NFT_PURCHASE:
return new TangleNftPurchaseService(this.transactionService);
case TangleRequestType.NFT_PURCHASE_BULK:
return new TangleNftPurchaseBulkService(this.transactionService);
case TangleRequestType.NFT_SET_FOR_SALE:
return new TangleNftSetForSaleService(this.transactionService);
case TangleRequestType.NFT_BID:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { baseTangleSchema } from '../common';

export const nftBidSchema = toJoiObject<NftBidTangleRequest>({
...baseTangleSchema(TangleRequestType.NFT_BID),
nft: CommonJoi.uid().description('Build5 if of the nft to bid on.'),
nft: CommonJoi.uid().description('Build5 id of the nft to bid on.'),
disableWithdraw: Joi.boolean().description(
"If set to true, NFT will not be sent to the buyer's validated address upon purchase.",
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
MAX_NFT_BULK_PURCHASE,
NftPurchaseBulkTangleRequest,
TangleRequestType,
} from '@build-5/interfaces';
import Joi from 'joi';
import { CommonJoi, toJoiObject } from '../../../joi/common';
import { baseTangleSchema } from '../common';

const nftPurchaseSchema = Joi.object({
collection: CommonJoi.uid().description(
'Build5 id of the collection in case a random nft is bought.',
),
nft: CommonJoi.uid(false).description('Build5 id of the nft to be purchased.'),
});

export const nftPurchaseBulkSchema = toJoiObject<NftPurchaseBulkTangleRequest>({
...baseTangleSchema(TangleRequestType.NFT_PURCHASE_BULK),
orders: Joi.array()
.items(nftPurchaseSchema)
.min(1)
.max(MAX_NFT_BULK_PURCHASE)
.description(
`List of collections&nfts to purchase, minimum 1, maximum ${MAX_NFT_BULK_PURCHASE}`,
)
.required(),
disableWithdraw: Joi.boolean().description(
"If set to true, NFT will not be sent to the buyer's validated address upon purchase.",
),
})
.description('Tangle request object to create an NFT bulk purchase order')
.meta({
className: 'NftPurchaseBulkTangleRequest',
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const nftPurchaseSchema = toJoiObject<NftPurchaseTangleRequest>({
collection: CommonJoi.uid().description(
'Build5 id of the collection in case a random nft is bought.',
),
nft: CommonJoi.uid(false).description('Build5 if of the nft to be purchased.'),
nft: CommonJoi.uid(false).description('Build5 id of the nft to be purchased.'),
disableWithdraw: Joi.boolean().description(
"If set to true, NFT will not be sent to the buyer's validated address upon purchase.",
),
Expand Down
Loading

0 comments on commit 0cfda60

Please sign in to comment.