Skip to content

Commit

Permalink
Merge pull request #2710 from build-5/market_buy_sell
Browse files Browse the repository at this point in the history
Market buy sell
  • Loading branch information
adamunchained authored Dec 13, 2023
2 parents fece8cb + 0d645d1 commit e30eb19
Show file tree
Hide file tree
Showing 29 changed files with 1,417 additions and 485 deletions.
448 changes: 279 additions & 169 deletions .github/workflows/functions_tangle-online-unit-tests_emulator.yml

Large diffs are not rendered by default.

456 changes: 283 additions & 173 deletions .github/workflows/functions_tangle-unit-tests.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/https/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import axios from 'axios';
import { ProjectWrapper } from './https';

export const https = (origin = Build5.PROD) => new HttpsWrapper(origin);
export const https = (origin: string = Build5.PROD) => new HttpsWrapper(origin as Build5);

class HttpsWrapper {
constructor(private readonly origin: Build5) {}
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/otr/datasets/TokenOtrDataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class TokenOtrDataset extends DatasetClass {
...params,
requestType: TangleRequestType.SELL_TOKEN,
},
Math.floor((params.count || 0) * params.price),
Math.floor((params.count || 0) * (params.price || 0)),
);
};

Expand All @@ -34,7 +34,7 @@ export class TokenOtrDataset extends DatasetClass {
new OtrRequest<TradeTokenTangleRequest>(
this.otrAddress,
{ ...params, requestType: TangleRequestType.BUY_TOKEN },
Math.floor((params.count || 0) * params.price),
Math.floor((params.count || 0) * (params.price || 0)),
);

stake = (tokenId: string, count: number, params: Omit<TokenStakeTangleRequest, 'requestType'>) =>
Expand Down
10 changes: 8 additions & 2 deletions packages/client/src/otr/datasets/common.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { Dataset, Network } from '@build-5/interfaces';
import { v4 as uuid } from 'uuid';
import { AuctionOtrDataset } from './AuctionOtrDataset';
import { AwardOtrDataset } from './AwardOtrDataset';
import { MemberOtrDataset } from './MemberOtrDataset';
import { NftOtrDataset } from './NftOtrDataset';
import { ProposalOtrDataset } from './ProposalOtrDataset';
import { SpaceOtrDataset } from './SpaceOtrDataset';
import { StamptOtrDataset } from './StampOtrDataset';
import { TokenOtrDataset } from './TokenOtrDataset';

// prettier-ignore
export type DatasetType<T extends Dataset> =
T extends Dataset.AUCTION ? AuctionOtrDataset:
T extends Dataset.AWARD ? AwardOtrDataset:
T extends Dataset.MEMBER ? MemberOtrDataset:
T extends Dataset.SPACE ? SpaceOtrDataset:
T extends Dataset.TOKEN ? TokenOtrDataset:
T extends Dataset.NFT ? NftOtrDataset:
T extends Dataset.PROPOSAL ? ProposalOtrDataset:
T extends Dataset.SPACE ? SpaceOtrDataset:
T extends Dataset.STAMP ? StamptOtrDataset:
T extends Dataset.TOKEN ? TokenOtrDataset:
unknown;

export interface INativeToken {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export const tradeMintedTokenSchema = toJoiObject<TradeTokenTangleRequest>({
.min(MIN_PRICE_PER_TOKEN)
.max(MAX_PRICE)
.precision(6)
.required()
.optional()
.description(
`Pirce of the token to trade. Minimum ${MIN_PRICE_PER_TOKEN}, maximum: ${MAX_PRICE}.`,
`Price of the token to trade. Minimum ${MIN_PRICE_PER_TOKEN}, maximum: ${MAX_PRICE}.`,
),
count: Joi.number()
.min(MIN_COUNT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { ITransaction, build5Db } from '@build-5/database';
import {
COL,
DEFAULT_NETWORK,
MAX_TOTAL_TOKEN_SUPPLY,
MIN_PRICE_PER_TOKEN,
Member,
Network,
SUB_COL,
Expand All @@ -13,6 +15,7 @@ import {
TokenTradeOrder,
TokenTradeOrderStatus,
TokenTradeOrderType,
TradeTokenTangleRequest,
Transaction,
TransactionPayloadType,
TransactionType,
Expand All @@ -22,9 +25,8 @@ import {
} from '@build-5/interfaces';
import dayjs from 'dayjs';
import bigDecimal from 'js-big-decimal';
import { set } from 'lodash';
import { head, set } from 'lodash';
import { assertMemberHasValidAddress } from '../../../../utils/address.utils';
import { packBasicOutput } from '../../../../utils/basic-output.utils';
import { getProject } from '../../../../utils/common.utils';
import { isProdEnv } from '../../../../utils/config.utils';
import { dateToTimestamp } from '../../../../utils/dateTime.utils';
Expand Down Expand Up @@ -73,8 +75,8 @@ export class TangleTokenTradeService extends BaseTangleService<TangleResponse> {
owner,
token,
type,
params.count || 0,
params.price,
getCount(params, type),
await getPrice(params, type, token.uid),
params.targetAddress,
'',
[TokenStatus.BASE, TokenStatus.MINTED],
Expand Down Expand Up @@ -113,6 +115,7 @@ const ACCEPTED_TOKEN_STATUSES = [
TokenStatus.MINTED,
TokenStatus.BASE,
];

export const createTokenTradeOrder = async (
project: string,
transaction: ITransaction,
Expand Down Expand Up @@ -232,12 +235,12 @@ const createTradeOrderTransaction = async (
network,
payload: {
type: isSell ? TransactionPayloadType.SELL_TOKEN : TransactionPayloadType.BUY_TOKEN,
amount: await getAmount(token, count, price, isSell),
amount: getAmount(token, count, price, isSell),
nativeTokens:
isMinted && isSell ? [{ id: token.mintingData?.tokenId!, amount: BigInt(count) }] : [],
isMinted && isSell ? [{ id: token.mintingData?.tokenId!, amount: BigInt(0) }] : [],
targetAddress: targetAddress.bech32,
expiresOn: dateToTimestamp(dayjs().add(TRANSACTION_MAX_EXPIRY_MS)),
validationType: getValidationType(token, isSell),
validationType: TransactionValidationType.ADDRESS,
reconciled: false,
void: false,
chainReference: null,
Expand All @@ -253,21 +256,45 @@ const createTradeOrderTransaction = async (
return order;
};

const getAmount = async (token: Token, count: number, price: number, isSell: boolean) => {
const getPrice = async (
params: TradeTokenTangleRequest,
type: TokenTradeOrderType,
token: string,
) => {
if (params.price) {
return params.price;
}
if (type === TokenTradeOrderType.SELL) {
return MIN_PRICE_PER_TOKEN;
}

const snap = await build5Db()
.collection(COL.TOKEN_MARKET)
.where('token', '==', token)
.where('status', '==', TokenTradeOrderStatus.ACTIVE)
.orderBy('price', 'desc')
.limit(1)
.get<TokenTradeOrder>();
const highestSell = head(snap);
if (!highestSell) {
throw invalidArgument(WenError.no_active_sells);
}
return highestSell.price;
};

const getCount = (params: TradeTokenTangleRequest, type: TokenTradeOrderType) => {
if (type === TokenTradeOrderType.BUY) {
return params.count || MAX_TOTAL_TOKEN_SUPPLY;
}
return params.count || 0;
};

const getAmount = (token: Token, count: number, price: number, isSell: boolean) => {
if (!isSell) {
return Number(bigDecimal.floor(bigDecimal.multiply(count, price)));
}
if (token.status !== TokenStatus.MINTED) {
return count;
}
const wallet = await WalletService.newWallet(token.mintingData?.network);
const tmpAddress = await wallet.getNewIotaAddressDetails(false);
const nativeTokens = [{ amount: BigInt(count), id: token.mintingData?.tokenId! }];
const output = await packBasicOutput(wallet, tmpAddress.bech32, 0, { nativeTokens });
return Number(output.amount);
return 0;
};

const getValidationType = (token: Token, isSell: boolean) =>
isSell && token.status === TokenStatus.MINTED
? TransactionValidationType.ADDRESS
: TransactionValidationType.ADDRESS_AND_AMOUNT;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { build5Db } from '@build-5/database';
import {
COL,
DEFAULT_NETWORK,
MAX_TOTAL_TOKEN_SUPPLY,
MilestoneTransactionEntry,
NativeToken,
SUB_COL,
TRANSACTION_MAX_EXPIRY_MS,
Expand All @@ -10,11 +12,12 @@ import {
TokenTradeOrder,
TokenTradeOrderStatus,
TokenTradeOrderType,
Transaction,
TransactionPayloadType,
getNetworkPair,
} from '@build-5/interfaces';
import dayjs from 'dayjs';
import { get, head, set } from 'lodash';
import { get, head, isEqual, set } from 'lodash';
import { dateToTimestamp } from '../../../utils/dateTime.utils';
import { getRandomEthAddress } from '../../../utils/wallet.utils';
import { BaseService, HandlerParams } from '../base';
Expand All @@ -29,11 +32,12 @@ export class TokenTradeService extends BaseService {
}: HandlerParams) => {
const payment = await this.transactionService.createPayment(order, match);

const nativeTokenId = head(order.payload.nativeTokens as NativeToken[])?.id;
const nativeTokens = nativeTokenId
? Number(tranEntry.nativeTokens?.find((n) => n.id === nativeTokenId)?.amount || 0)
: 0;
if (nativeTokenId && (!nativeTokens || (tranEntry.nativeTokens?.length || 0) > 1)) {
const expectedNativeTokenIds = [head(order.payload.nativeTokens as NativeToken[])?.id].filter(
(n) => n !== undefined,
);
const receivedNativeTokenIds = tranEntry.nativeTokens?.map((n) => n.id);

if (!isEqual(expectedNativeTokenIds, receivedNativeTokenIds)) {
await this.transactionService.createCredit(
TransactionPayloadType.INVALID_AMOUNT,
payment,
Expand All @@ -42,24 +46,30 @@ export class TokenTradeService extends BaseService {
return;
}
this.transactionService.markAsReconciled(order, match.msgId);
const nativeTokens = Number(head(tranEntry.nativeTokens)?.amount);

await this.createDistributionDocRef(order.payload.token!, order.member!);
const token = <Token>await build5Db().doc(`${COL.TOKEN}/${order.payload.token}`).get();
const network = order.network || DEFAULT_NETWORK;

const type =
order.payload.type === TransactionPayloadType.SELL_TOKEN
? TokenTradeOrderType.SELL
: TokenTradeOrderType.BUY;
const price = get(order, 'payload.price', 0);
const count = getCount(order, tranEntry, type);

const data: TokenTradeOrder = {
project,
uid: getRandomEthAddress(),
owner: order.member!,
token: token.uid,
tokenStatus: token.status,
type:
order.payload.type === TransactionPayloadType.SELL_TOKEN
? TokenTradeOrderType.SELL
: TokenTradeOrderType.BUY,
count: nativeTokens || get(order, 'payload.count', 0),
price: get(order, 'payload.price', 0),
totalDeposit: nativeTokens || order.payload.amount || 0,
balance: nativeTokens || order.payload.amount || 0,
type,
count,
price,
totalDeposit: nativeTokens || match.to.amount || 0,
balance: nativeTokens || match.to.amount || 0,
fulfilled: 0,
status: TokenTradeOrderStatus.ACTIVE,
orderTransactionId: order.uid,
Expand All @@ -77,17 +87,12 @@ export class TokenTradeService extends BaseService {
const ref = build5Db().doc(`${COL.TOKEN_MARKET}/${data.uid}`);
this.transactionService.push({ ref, data, action: 'set' });

if (
order.payload.type === TransactionPayloadType.SELL_TOKEN &&
token.status === TokenStatus.MINTED
) {
const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${order.uid}`);
this.transactionService.push({
ref: orderDocRef,
data: { 'payload.amount': match.to.amount },
action: 'update',
});
}
const orderDocRef = build5Db().doc(`${COL.TRANSACTION}/${order.uid}`);
this.transactionService.push({
ref: orderDocRef,
data: { 'payload.amount': match.to.amount, 'payload.count': count },
action: 'update',
});
};

private createDistributionDocRef = async (token: string, member: string) => {
Expand All @@ -110,3 +115,14 @@ export class TokenTradeService extends BaseService {
}
};
}

const getCount = (
order: Transaction,
tranEntry: MilestoneTransactionEntry,
type: TokenTradeOrderType,
) => {
if (type === TokenTradeOrderType.SELL) {
return Number(head(tranEntry.nativeTokens)?.amount || 0) || tranEntry.amount;
}
return get(order, 'payload.count', MAX_TOTAL_TOKEN_SUPPLY);
};
17 changes: 10 additions & 7 deletions packages/functions/src/triggers/token-trading/match-base-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ITransaction, build5Db } from '@build-5/database';
import {
COL,
Entity,
MIN_IOTA_AMOUNT,
Member,
Space,
Token,
Expand Down Expand Up @@ -33,11 +32,11 @@ const createIotaPayments = async (
buyer: Member,
count: number,
): Promise<Transaction[]> => {
if (count < MIN_IOTA_AMOUNT) {
return [];
}
const balance = sell.balance - count;
if (balance !== 0 && balance < MIN_IOTA_AMOUNT) {
const wallet = await WalletService.newWallet(sell.sourceNetwork!);
const tmpAddress = await wallet.getNewIotaAddressDetails(false);
const remainder = await packBasicOutput(wallet, tmpAddress.bech32, balance, {});
if (balance !== 0 && balance < Number(remainder.amount)) {
return [];
}
const sellOrder = await build5Db()
Expand Down Expand Up @@ -156,9 +155,9 @@ const createSmrPayments = async (
.doc(`${COL.TRANSACTION}/${buy.orderTransactionId}`)
.get<Transaction>();

const fulfilled = buy.fulfilled + tokensToTrade === buy.count;
let salePrice = Number(bigDecimal.floor(bigDecimal.multiply(price, tokensToTrade)));
let balanceLeft = buy.balance - salePrice;
const fulfilled = buy.fulfilled + tokensToTrade === buy.count || balanceLeft === 0;

if (balanceLeft < 0) {
return [];
Expand Down Expand Up @@ -252,7 +251,11 @@ export const matchBaseToken = async (
price: number,
triggeredBy: TokenTradeOrderType,
): Promise<Match> => {
const tokensToTrade = Math.min(sell.count - sell.fulfilled, buy.count - buy.fulfilled);
const tokensToTrade = Math.min(
sell.count - sell.fulfilled,
buy.count - buy.fulfilled,
Math.floor(buy.balance / price),
);
const seller = await build5Db().doc(`${COL.MEMBER}/${sell.owner}`).get<Member>();
const buyer = await build5Db().doc(`${COL.MEMBER}/${buy.owner}`).get<Member>();

Expand Down
Loading

0 comments on commit e30eb19

Please sign in to comment.