Skip to content

Commit d2d18a2

Browse files
committed
test: add e2e tests for metadata bids
1 parent ca3074e commit d2d18a2

3 files changed

Lines changed: 339 additions & 2 deletions

File tree

packages/orderbook/src/test/helpers/order.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Orderbook } from '../../orderbook';
2-
import { Order, OrderStatusName } from '../../types';
2+
import { MetadataBid, Order, OrderStatusName } from '../../types';
33

44
export async function waitForOrderToBeOfStatus(
55
sdk: Orderbook,
@@ -20,3 +20,23 @@ export async function waitForOrderToBeOfStatus(
2020
await new Promise((resolve) => setTimeout(resolve, 1000));
2121
return waitForOrderToBeOfStatus(sdk, orderId, status, attemps + 1);
2222
}
23+
24+
export async function waitForMetadataBidToBeOfStatus(
25+
sdk: Orderbook,
26+
metadataBidId: string,
27+
status: OrderStatusName,
28+
attempts = 0,
29+
): Promise<MetadataBid> {
30+
if (attempts > 50) {
31+
throw new Error(`Metadata bid ${metadataBidId} never reached status ${status}`);
32+
}
33+
34+
const { result: bid } = await sdk.getMetadataBid(metadataBidId);
35+
if (bid.status.name === status) {
36+
return bid;
37+
}
38+
39+
// eslint-disable-next-line
40+
await new Promise((resolve) => setTimeout(resolve, 1000));
41+
return waitForMetadataBidToBeOfStatus(sdk, metadataBidId, status, attempts + 1);
42+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { hexlify, randomBytes } from 'ethers';
22

33
export function getRandomTokenId(): string {
4-
return BigInt(`0x${hexlify(randomBytes(4))}`).toString(10);
4+
return BigInt(`${hexlify(randomBytes(4))}`).toString(10);
55
}
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import { randomUUID, randomBytes } from 'crypto';
2+
import { execSync } from 'child_process';
3+
import { Contract } from 'ethers';
4+
import { Environment } from '@imtbl/config';
5+
import { OrderStatusName } from '../openapi/sdk';
6+
import { Orderbook } from '../orderbook';
7+
import { getLocalhostProvider } from './helpers/provider';
8+
import { getOffererWallet, getFulfillerWallet } from './helpers/signers';
9+
import { waitForMetadataBidToBeOfStatus } from './helpers/order';
10+
import { getConfigFromEnv, getRandomTokenId } from './helpers';
11+
import { actionAll } from './helpers/actions';
12+
import { GAS_OVERRIDES } from './helpers/gas';
13+
14+
const LOCAL_CHAIN_ID = 'eip155:31337';
15+
const LOCAL_CHAIN_NAME = 'imtbl-zkevm-local';
16+
17+
const ERC20_ABI = [
18+
'function mint(address to, uint256 amount) external',
19+
'function approve(address spender, uint256 amount) external returns (bool)',
20+
'function balanceOf(address) external view returns (uint256)',
21+
];
22+
23+
const ERC721_ABI = [
24+
'function safeMint(address to, uint256 tokenId) external',
25+
'function setApprovalForAll(address operator, bool approved) external',
26+
'function ownerOf(uint256 tokenId) external view returns (address)',
27+
];
28+
29+
/**
30+
* Seeds the indexer-mr database so that the given NFT is associated with a metadata_id.
31+
* This allows the fulfillment endpoint to validate the token against the metadata bid.
32+
*/
33+
function ensureIndexerNft(
34+
contractAddress: string,
35+
tokenId: string,
36+
): string {
37+
const port = process.env.INDEXER_MR_POSTGRES_PORT ?? '5434';
38+
const connStr = `postgres://postgres:postgres@localhost:${port}/indexer-mr`;
39+
const metadataId = randomUUID();
40+
const metadataHash = randomBytes(16).toString('hex');
41+
const nftId = randomUUID();
42+
43+
const sql = `
44+
BEGIN;
45+
46+
INSERT INTO chains (id, name, rpc_url, operator_allowlist_address, minter_address)
47+
VALUES ('${LOCAL_CHAIN_ID}', '${LOCAL_CHAIN_NAME}', 'http://127.0.0.1:8545',
48+
'0x0000000000000000000000000000000000000000',
49+
'0x0000000000000000000000000000000000000001')
50+
ON CONFLICT (id) DO NOTHING;
51+
52+
INSERT INTO collections
53+
(chain_id, contract_address, contract_type, indexed_at, updated_at, verification_status)
54+
VALUES
55+
('${LOCAL_CHAIN_ID}', LOWER('${contractAddress}'), 'erc721'::collections_contract_type,
56+
NOW(), NOW(), 'unverified'::asset_verification_status)
57+
ON CONFLICT (chain_id, contract_address) DO NOTHING;
58+
59+
INSERT INTO nft_metadata
60+
(id, chain_id, contract_address, hash, name, image, attributes, created_at, updated_at)
61+
VALUES
62+
('${metadataId}'::uuid, '${LOCAL_CHAIN_ID}', LOWER('${contractAddress}'),
63+
'${metadataHash}', 'e2e metadata-bid nft', 'https://example.com/nft.png',
64+
'[]'::jsonb, NOW(), NOW());
65+
66+
INSERT INTO nfts
67+
(id, chain_id, contract_address, token_id, contract_type, indexed_at, updated_at, metadata_id)
68+
VALUES
69+
('${nftId}'::uuid, '${LOCAL_CHAIN_ID}', LOWER('${contractAddress}'),
70+
${tokenId}::numeric, 'erc721'::collections_contract_type,
71+
NOW(), NOW(), '${metadataId}'::uuid)
72+
ON CONFLICT (chain_id, contract_address, token_id) DO UPDATE SET
73+
metadata_id = EXCLUDED.metadata_id,
74+
updated_at = EXCLUDED.updated_at;
75+
76+
COMMIT;
77+
`;
78+
79+
execSync(`psql "${connStr}" -c "${sql.replace(/\n/g, ' ')}"`, { stdio: 'pipe' });
80+
return metadataId;
81+
}
82+
83+
describe('metadata bid e2e', () => {
84+
const provider = getLocalhostProvider();
85+
const maker = getOffererWallet(provider);
86+
const taker = getFulfillerWallet(provider);
87+
88+
const localConfigOverrides = getConfigFromEnv();
89+
const sdk = new Orderbook({
90+
baseConfig: {
91+
environment: Environment.SANDBOX,
92+
},
93+
overrides: {
94+
...localConfigOverrides,
95+
},
96+
});
97+
98+
let erc721Address: string;
99+
let erc20Address: string;
100+
101+
beforeAll(async () => {
102+
erc721Address = process.env.ZKEVM_ORDERBOOK_ERC721!;
103+
if (!erc721Address) {
104+
throw new Error('ZKEVM_ORDERBOOK_ERC721 must be set');
105+
}
106+
107+
erc20Address = process.env.ZKEVM_ORDERBOOK_ERC20!;
108+
if (!erc20Address) {
109+
throw new Error('ZKEVM_ORDERBOOK_ERC20 must be set');
110+
}
111+
112+
const erc20 = new Contract(erc20Address, ERC20_ABI, maker);
113+
114+
const mintTx = await erc20.mint(maker.address, BigInt('1000000000000000000'), GAS_OVERRIDES);
115+
await mintTx.wait();
116+
}, 60_000);
117+
118+
it('should prepare, create, and get a metadata bid', async () => {
119+
const metadataId = randomUUID();
120+
121+
const prepareResult = await sdk.prepareMetadataBid({
122+
makerAddress: maker.address,
123+
sell: {
124+
contractAddress: erc20Address,
125+
amount: '1000000',
126+
type: 'ERC20',
127+
},
128+
buy: {
129+
contractAddress: erc721Address,
130+
type: 'ERC721_COLLECTION',
131+
amount: '1',
132+
},
133+
});
134+
135+
const signatures = await actionAll(prepareResult.actions, maker);
136+
137+
const { result: createdBid } = await sdk.createMetadataBid({
138+
orderComponents: prepareResult.orderComponents,
139+
orderHash: prepareResult.orderHash,
140+
orderSignature: signatures[0],
141+
makerFees: [],
142+
metadataId,
143+
});
144+
145+
expect(createdBid.id).toBeDefined();
146+
expect(createdBid.type).toEqual('METADATA_BID');
147+
expect(createdBid.metadataId).toEqual(metadataId);
148+
149+
const activeBid = await waitForMetadataBidToBeOfStatus(
150+
sdk,
151+
createdBid.id,
152+
OrderStatusName.ACTIVE,
153+
);
154+
155+
expect(activeBid.id).toEqual(createdBid.id);
156+
expect(activeBid.metadataId).toEqual(metadataId);
157+
expect(activeBid.status.name).toEqual(OrderStatusName.ACTIVE);
158+
}, 60_000);
159+
160+
it('should list metadata bids filtered by account address', async () => {
161+
const metadataId = randomUUID();
162+
163+
const prepareResult = await sdk.prepareMetadataBid({
164+
makerAddress: maker.address,
165+
sell: {
166+
contractAddress: erc20Address,
167+
amount: '500000',
168+
type: 'ERC20',
169+
},
170+
buy: {
171+
contractAddress: erc721Address,
172+
type: 'ERC721_COLLECTION',
173+
amount: '1',
174+
},
175+
});
176+
177+
const signatures = await actionAll(prepareResult.actions, maker);
178+
179+
const { result: createdBid } = await sdk.createMetadataBid({
180+
orderComponents: prepareResult.orderComponents,
181+
orderHash: prepareResult.orderHash,
182+
orderSignature: signatures[0],
183+
makerFees: [],
184+
metadataId,
185+
});
186+
187+
await waitForMetadataBidToBeOfStatus(
188+
sdk,
189+
createdBid.id,
190+
OrderStatusName.ACTIVE,
191+
);
192+
193+
const listResult = await sdk.listMetadataBids({
194+
accountAddress: maker.address,
195+
status: OrderStatusName.ACTIVE,
196+
});
197+
198+
expect(listResult.result.length).toBeGreaterThan(0);
199+
200+
const found = listResult.result.find((bid) => bid.id === createdBid.id);
201+
expect(found).toBeDefined();
202+
expect(found!.type).toEqual('METADATA_BID');
203+
expect(found!.metadataId).toEqual(metadataId);
204+
}, 60_000);
205+
206+
it('should list metadata bids filtered by buy item contract address', async () => {
207+
const metadataId = randomUUID();
208+
209+
const prepareResult = await sdk.prepareMetadataBid({
210+
makerAddress: maker.address,
211+
sell: {
212+
contractAddress: erc20Address,
213+
amount: '300000',
214+
type: 'ERC20',
215+
},
216+
buy: {
217+
contractAddress: erc721Address,
218+
type: 'ERC721_COLLECTION',
219+
amount: '1',
220+
},
221+
});
222+
223+
const signatures = await actionAll(prepareResult.actions, maker);
224+
225+
const { result: createdBid } = await sdk.createMetadataBid({
226+
orderComponents: prepareResult.orderComponents,
227+
orderHash: prepareResult.orderHash,
228+
orderSignature: signatures[0],
229+
makerFees: [],
230+
metadataId,
231+
});
232+
233+
await waitForMetadataBidToBeOfStatus(
234+
sdk,
235+
createdBid.id,
236+
OrderStatusName.ACTIVE,
237+
);
238+
239+
const listResult = await sdk.listMetadataBids({
240+
buyItemContractAddress: erc721Address,
241+
status: OrderStatusName.ACTIVE,
242+
});
243+
244+
expect(listResult.result.length).toBeGreaterThan(0);
245+
246+
const found = listResult.result.find((bid) => bid.id === createdBid.id);
247+
expect(found).toBeDefined();
248+
expect(found!.metadataId).toEqual(metadataId);
249+
}, 60_000);
250+
251+
it('should fulfill a metadata bid', async () => {
252+
const tokenId = getRandomTokenId();
253+
254+
const erc721 = new Contract(erc721Address, ERC721_ABI, maker);
255+
const mintTx = await erc721.safeMint(taker.address, tokenId, GAS_OVERRIDES);
256+
await mintTx.wait();
257+
258+
const metadataId = ensureIndexerNft(erc721Address, tokenId);
259+
260+
const takerErc721 = new Contract(erc721Address, ERC721_ABI, taker);
261+
const seaportAddress = process.env.SEAPORT_CONTRACT_ADDRESS!;
262+
const approvalTx = await takerErc721.setApprovalForAll(seaportAddress, true, GAS_OVERRIDES);
263+
await approvalTx.wait();
264+
265+
const blockTime = await provider.getBlock('latest');
266+
267+
const prepareResult = await sdk.prepareMetadataBid({
268+
makerAddress: maker.address,
269+
sell: {
270+
contractAddress: erc20Address,
271+
amount: '100000',
272+
type: 'ERC20',
273+
},
274+
buy: {
275+
contractAddress: erc721Address,
276+
type: 'ERC721_COLLECTION',
277+
amount: '1',
278+
},
279+
orderStart: new Date(blockTime!.timestamp - 1000),
280+
});
281+
282+
const signatures = await actionAll(prepareResult.actions, maker);
283+
284+
const { result: createdBid } = await sdk.createMetadataBid({
285+
orderComponents: prepareResult.orderComponents,
286+
orderHash: prepareResult.orderHash,
287+
orderSignature: signatures[0],
288+
makerFees: [],
289+
metadataId,
290+
});
291+
292+
await waitForMetadataBidToBeOfStatus(
293+
sdk,
294+
createdBid.id,
295+
OrderStatusName.ACTIVE,
296+
);
297+
298+
const fulfillment = await sdk.fulfillOrder(
299+
createdBid.id,
300+
taker.address,
301+
[],
302+
undefined,
303+
tokenId,
304+
);
305+
306+
await actionAll(fulfillment.actions, taker);
307+
308+
const filledBid = await waitForMetadataBidToBeOfStatus(
309+
sdk,
310+
createdBid.id,
311+
OrderStatusName.FILLED,
312+
);
313+
314+
expect(filledBid.id).toEqual(createdBid.id);
315+
expect(filledBid.status.name).toEqual(OrderStatusName.FILLED);
316+
}, 120_000);
317+
});

0 commit comments

Comments
 (0)