Skip to content

Commit 3804a15

Browse files
authored
Merge pull request #26 from trader-xyz/feat/orderbook
Open Orderbook functionality
2 parents 00335dd + 0d64f71 commit 3804a15

File tree

9 files changed

+455
-8
lines changed

9 files changed

+455
-8
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"@ethersproject/wallet": "^5.5.0",
7171
"date-fns": "^2.28.0",
7272
"ethers": "^5.5.4",
73-
"from-exponential": "^1.1.1",
73+
"isomorphic-unfetch": "^3.1.0",
7474
"lodash": "^4.17.21",
7575
"lodash-es": "^4.17.21",
7676
"tiny-invariant": "^1.2.0",

src/sdk/v4/NftSwapV4.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,20 @@ import type {
4545
NftOrderV4Serialized,
4646
OrderStructOptionsCommonStrict,
4747
SignedNftOrderV4,
48+
SignedNftOrderV4Serialized,
4849
SigningOptions,
4950
} from './types';
5051
import {
5152
ERC1155_TRANSFER_FROM_DATA,
5253
ERC721_TRANSFER_FROM_DATA,
5354
} from './nft-safe-transfer-from-data';
5455
import addresses from './addresses.json';
56+
import {
57+
ORDERBOOK_API_ROOT_URL_PRODUCTION,
58+
postOrderToOrderbook,
59+
searchOrderbook,
60+
SearchOrdersParams,
61+
} from './orderbook';
5562

5663
export enum SupportedChainIdsV4 {
5764
Ropsten = 3,
@@ -121,6 +128,7 @@ export interface INftSwapV4 extends BaseNftSwap {
121128

122129
export interface AdditionalSdkConfig {
123130
zeroExExchangeProxyContractAddress: string;
131+
orderbookRootUrl: string;
124132
}
125133

126134
class NftSwapV4 implements INftSwapV4 {
@@ -130,6 +138,8 @@ class NftSwapV4 implements INftSwapV4 {
130138
public exchangeProxy: IZeroEx;
131139
public exchangeProxyContractAddress: string;
132140

141+
public orderbookRootUrl: string;
142+
133143
constructor(
134144
provider: BaseProvider,
135145
signer: Signer,
@@ -155,6 +165,9 @@ class NftSwapV4 implements INftSwapV4 {
155165

156166
this.exchangeProxyContractAddress = zeroExExchangeContractAddress;
157167

168+
this.orderbookRootUrl =
169+
additionalConfig?.orderbookRootUrl ?? ORDERBOOK_API_ROOT_URL_PRODUCTION;
170+
158171
this.exchangeProxy = IZeroEx__factory.connect(
159172
zeroExExchangeContractAddress,
160173
signer ?? provider
@@ -567,6 +580,23 @@ class NftSwapV4 implements INftSwapV4 {
567580
console.log('unsupported order', signedOrder);
568581
throw new Error('unsupport signedOrder type');
569582
};
583+
584+
postOrder = (
585+
signedOrder: SignedNftOrderV4,
586+
chainId: string,
587+
metadata?: Record<string, string>
588+
) => {
589+
postOrderToOrderbook(signedOrder, chainId, metadata, {
590+
rootUrl: this.orderbookRootUrl,
591+
});
592+
};
593+
594+
getOrders = async (filters?: Partial<SearchOrdersParams>) => {
595+
const orders = await searchOrderbook(filters, {
596+
rootUrl: this.orderbookRootUrl,
597+
});
598+
return orders;
599+
};
570600
}
571601

572602
export { NftSwapV4 };

src/sdk/v4/orderbook.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import unfetch from 'isomorphic-unfetch';
2+
import type { SignedNftOrderV4, SignedNftOrderV4Serialized } from './types';
3+
import { stringify } from '../../utils/query-string';
4+
import { serializeNftOrder } from './pure';
5+
6+
export const ORDERBOOK_API_ROOT_URL_PRODUCTION = 'https://api.trader.xyz';
7+
8+
export interface OrderbookRequestOptions {
9+
rootUrl: string;
10+
}
11+
12+
export interface PostOrderRequestPayload {
13+
order: SignedNftOrderV4Serialized;
14+
chainId: string;
15+
metadata?: Record<string, string>;
16+
}
17+
18+
export interface OrderDataPayload {
19+
erc20Token: string;
20+
erc20TokenAmount: string;
21+
nftToken: string;
22+
nftTokenId: string;
23+
nftTokenAmount: string;
24+
nftType: string;
25+
sellOrBuyNft: 'buy' | 'sell';
26+
chainId: string;
27+
order: SignedNftOrderV4Serialized;
28+
metadata: Record<string, string> | null;
29+
}
30+
31+
export type PostOrderResponsePayload = OrderDataPayload;
32+
33+
export interface SearchOrdersResponsePayload {
34+
orders: Array<OrderDataPayload>;
35+
}
36+
37+
const postOrderToOrderbook = async (
38+
signedOrder: SignedNftOrderV4,
39+
chainId: string,
40+
metadata: Record<string, string> = {},
41+
requestOptions?: Partial<OrderbookRequestOptions>,
42+
fetchFn: typeof unfetch = unfetch
43+
): Promise<PostOrderResponsePayload> => {
44+
const payload: PostOrderRequestPayload = {
45+
order: serializeNftOrder(signedOrder),
46+
chainId,
47+
metadata,
48+
};
49+
50+
let rootUrl = requestOptions?.rootUrl ?? ORDERBOOK_API_ROOT_URL_PRODUCTION;
51+
52+
const orderPostResult: PostOrderResponsePayload = await fetchFn(
53+
`${rootUrl}/orderbook/order`,
54+
{
55+
method: 'post',
56+
headers: {
57+
'Content-Type': 'application/json',
58+
},
59+
body: JSON.stringify(payload),
60+
}
61+
)
62+
.then(async (res) => {
63+
if (!res.ok) {
64+
throw await res.json();
65+
}
66+
if (res.status >= 300) {
67+
throw await res.json();
68+
}
69+
return res.json();
70+
})
71+
.catch((err) => {
72+
// err is not a promise
73+
throw err;
74+
});
75+
76+
return orderPostResult;
77+
};
78+
79+
export interface SearchOrdersParams {
80+
erc20Token: string;
81+
nftTokenId: string;
82+
nftToken: string;
83+
nftType: string;
84+
chainId: string;
85+
maker: string;
86+
taker: string;
87+
nonce: string;
88+
}
89+
90+
const searchOrderbook = async (
91+
filters?: Partial<SearchOrdersParams>,
92+
requestOptions?: Partial<OrderbookRequestOptions>,
93+
fetchFn: typeof unfetch = unfetch
94+
): Promise<SearchOrdersResponsePayload> => {
95+
const stringifiedQueryParams = stringify(filters ?? {});
96+
97+
let rootUrl = requestOptions?.rootUrl ?? ORDERBOOK_API_ROOT_URL_PRODUCTION;
98+
99+
const findOrdersResult = await fetchFn(
100+
`${rootUrl}/orderbook/orders?${stringifiedQueryParams}`
101+
)
102+
.then(async (res) => {
103+
if (!res.ok) {
104+
throw await res.json();
105+
}
106+
if (res.status >= 300) {
107+
throw await res.json();
108+
}
109+
return res.json();
110+
})
111+
.catch((err) => {
112+
// err is not a promise
113+
throw err;
114+
});
115+
116+
return findOrdersResult;
117+
};
118+
119+
export { postOrderToOrderbook, searchOrderbook };

src/sdk/v4/pure.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import type {
1515
ECSignature,
1616
ERC1155OrderStruct,
1717
ERC1155OrderStructSerialized,
18-
ERC721OrderStruct,
1918
ERC721OrderStructSerialized,
2019
NftOrderV4,
2120
OrderStructOptionsCommon,
2221
OrderStructOptionsCommonStrict,
2322
PropertyStruct,
23+
SignedNftOrderV4,
24+
SignedNftOrderV4Serialized,
2425
} from './types';
2526
import { BaseProvider } from '@ethersproject/providers';
2627
import { ApprovalStatus, TransactionOverrides } from '../common/types';
@@ -416,3 +417,44 @@ export const CONTRACT_ORDER_VALIDATOR: PropertyStruct = {
416417
propertyValidator: NULL_ADDRESS,
417418
propertyData: [],
418419
};
420+
421+
export const serializeNftOrder = (
422+
signedOrder: SignedNftOrderV4
423+
): SignedNftOrderV4Serialized => {
424+
if ('erc721Token' in signedOrder) {
425+
return {
426+
...signedOrder,
427+
direction: parseInt(signedOrder.direction.toString()),
428+
expiry: signedOrder.expiry.toString(),
429+
nonce: signedOrder.nonce.toString(),
430+
erc20TokenAmount: signedOrder.erc20TokenAmount.toString(),
431+
fees: signedOrder.fees.map((fee) => ({
432+
...fee,
433+
amount: fee.amount.toString(),
434+
feeData: fee.feeData.toString(),
435+
})),
436+
erc721TokenId: signedOrder.erc721TokenId.toString(),
437+
};
438+
} else if ('erc1155Token' in signedOrder) {
439+
return {
440+
...signedOrder,
441+
direction: parseInt(signedOrder.direction.toString()),
442+
expiry: signedOrder.expiry.toString(),
443+
nonce: signedOrder.nonce.toString(),
444+
erc20TokenAmount: signedOrder.erc20TokenAmount.toString(),
445+
fees: signedOrder.fees.map((fee) => ({
446+
...fee,
447+
amount: fee.amount.toString(),
448+
feeData: fee.feeData.toString(),
449+
})),
450+
erc1155TokenAmount: signedOrder.erc1155TokenAmount.toString(),
451+
erc1155TokenId: signedOrder.erc1155TokenId.toString(),
452+
};
453+
} else {
454+
console.log(
455+
'unknown order format type (not erc721 and not erc1155',
456+
signedOrder
457+
);
458+
throw new Error('Unknown asset type');
459+
}
460+
};

src/utils/bn/fromExponential.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Return two parts array of exponential number
3+
* @param {number|string|Array} num
4+
* @return {string[]}
5+
*/
6+
export function getExponentialParts(num: number | string | Array<string>) {
7+
return Array.isArray(num) ? num : String(num).split(/[eE]/);
8+
}
9+
10+
/**
11+
*
12+
* @param {number|string|Array} num - number or array of its parts
13+
*/
14+
export function isExponential(num: number | string | Array<string>) {
15+
const eParts = getExponentialParts(num);
16+
return !Number.isNaN(Number(eParts[1]));
17+
}
18+
19+
/**
20+
* Converts exponential notation to a human readable string
21+
* @param {number|string|Array} num - number or array of its parts
22+
* @return {string}
23+
*/
24+
export function fromExponential(num: number | string | Array<string>) {
25+
const eParts = getExponentialParts(num);
26+
if (!isExponential(eParts)) {
27+
return eParts[0];
28+
}
29+
30+
const sign = eParts[0][0] === '-' ? '-' : '';
31+
const digits = eParts[0].replace(/^-/, '');
32+
const digitsParts = digits.split('.');
33+
const wholeDigits = digitsParts[0];
34+
const fractionDigits = digitsParts[1] || '';
35+
let e = Number(eParts[1]);
36+
37+
if (e === 0) {
38+
return `${sign + wholeDigits}.${fractionDigits}`;
39+
} else if (e < 0) {
40+
// move dot to the left
41+
const countWholeAfterTransform = wholeDigits.length + e;
42+
if (countWholeAfterTransform > 0) {
43+
// transform whole to fraction
44+
const wholeDigitsAfterTransform = wholeDigits.substr(
45+
0,
46+
countWholeAfterTransform
47+
);
48+
const wholeDigitsTransformedToFraction = wholeDigits.substr(
49+
countWholeAfterTransform
50+
);
51+
return `${
52+
sign + wholeDigitsAfterTransform
53+
}.${wholeDigitsTransformedToFraction}${fractionDigits}`;
54+
} else {
55+
// not enough whole digits: prepend with fractional zeros
56+
57+
// first e goes to dotted zero
58+
let zeros = '0.';
59+
e = countWholeAfterTransform;
60+
while (e) {
61+
zeros += '0';
62+
e += 1;
63+
}
64+
return sign + zeros + wholeDigits + fractionDigits;
65+
}
66+
} else {
67+
// move dot to the right
68+
const countFractionAfterTransform = fractionDigits.length - e;
69+
if (countFractionAfterTransform > 0) {
70+
// transform fraction to whole
71+
// countTransformedFractionToWhole = e
72+
const fractionDigitsAfterTransform = fractionDigits.substr(e);
73+
const fractionDigitsTransformedToWhole = fractionDigits.substr(0, e);
74+
return `${
75+
sign + wholeDigits + fractionDigitsTransformedToWhole
76+
}.${fractionDigitsAfterTransform}`;
77+
} else {
78+
// not enough fractions: append whole zeros
79+
let zerosCount = -countFractionAfterTransform;
80+
let zeros = '';
81+
while (zerosCount) {
82+
zeros += '0';
83+
zerosCount -= 1;
84+
}
85+
return sign + wholeDigits + fractionDigits + zeros;
86+
}
87+
}
88+
}

src/utils/bn/toBN.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// From https://github.com/paulrberg/evm-bn
22
import type { BigNumber } from '@ethersproject/bignumber';
33
import { parseFixed } from '@ethersproject/bignumber';
4-
import fromExponential from 'from-exponential';
4+
import { fromExponential } from './fromExponential';
55

66
/**
77
* Convert a stringified fixed-point number to a big number with a custom number of decimals.

src/utils/query-string.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Simple (tiny) query-string helpers
2+
3+
const parse = (
4+
str: string,
5+
decode: typeof decodeURIComponent = decodeURIComponent
6+
) => {
7+
return (str + '')
8+
.replace(/\+/g, ' ')
9+
.split('&')
10+
.filter(Boolean)
11+
.reduce(function (obj: Record<string, any>, item) {
12+
const ref = item.split('=');
13+
const key = decode(ref[0] || '');
14+
const val = decode(ref[1] || '');
15+
const prev = obj[key];
16+
obj[key] =
17+
prev === undefined ? val : ([] as Array<any>).concat(prev, val);
18+
return obj;
19+
}, {});
20+
};
21+
22+
const stringify = (
23+
obj: Record<string, any>,
24+
encode: typeof encodeURIComponent = encodeURIComponent
25+
) => {
26+
return Object.keys(obj || {})
27+
.reduce(function (arr: any[], key) {
28+
[].concat(obj[key]).forEach(function (v) {
29+
arr.push(encode(key) + '=' + encode(v));
30+
});
31+
return arr;
32+
}, [])
33+
.join('&')
34+
.replace(/\s/g, '+');
35+
};
36+
37+
export { parse, stringify };

0 commit comments

Comments
 (0)