Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bulk nft purchase #2747

Merged
merged 1 commit into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading