diff --git a/package.json b/package.json index f06f21ca57..4743a88c20 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "@actions/core": "^1.10.1", "@emotion/react": "^11.11.3", "@release-it-plugins/workspaces": "^4.0.0", + "@types/chai": "^4.3.16", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", + "chai": "^5.1.1", "eslint": "^8.40.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", diff --git a/packages/orderbook/package.json b/packages/orderbook/package.json index 37e4ef2583..d6e888a7c2 100644 --- a/packages/orderbook/package.json +++ b/packages/orderbook/package.json @@ -15,6 +15,7 @@ "@swc/jest": "^0.2.24", "@typechain/ethers-v5": "^10.2.0", "@types/jest": "^29.4.3", + "chai": "^5.1.1", "dotenv": "^16.0.3", "eslint": "^8.40.0", "jest": "^29.4.3", diff --git a/packages/orderbook/src/seaport/fillable-units.spec.ts b/packages/orderbook/src/seaport/fillable-units.spec.ts new file mode 100644 index 0000000000..b4868e3eb4 --- /dev/null +++ b/packages/orderbook/src/seaport/fillable-units.spec.ts @@ -0,0 +1,116 @@ +import { expect } from 'chai'; +import { Fee, Order, ProtocolData } from 'openapi/sdk'; +import { determineFillableUnits } from './fillable-units'; + +describe('determineFillableUnits', () => { + function createOrder(overrides?: Partial): Order { + // Default order object + const defaultOrder: Order = { + sell: [ + { + type: 'ERC1155', + amount: '100', + contract_address: '0xEEb7Da6De152597830eD16361633e362A2F59410', + token_id: '123', + }, + ], + buy: [ + { + type: 'ERC20', + amount: '100', + contract_address: '0xFFb7Da6De152597830eD16361633e362A2F59411', + }, + ], + type: Order.type.LISTING, + fill_status: { + numerator: '0', + denominator: '0', + }, + account_address: '0x1237Da6De152597830eD16361633e362A2F59412', + fees: [ + { + type: Fee.type.PROTOCOL, + amount: '100', + recipient_address: '0x4567Da6De152597830eD16361633e362A2F59413', + }, + ], + chain: { + id: 'eip155:13473', + name: 'imtbl-zkevm-testnet', + }, + created_at: '2024-06-18T06:16:57.902738Z', + end_at: '2026-06-18T06:16:14Z', + id: '019029fd-cf21-0a33-c77f-e121f5162f22', + order_hash: '0xba8ebe0b4ac6f1cc21a2274199b238959aaa0c59e1f2b31a8b7e8a66bf9f9635', + protocol_data: { + order_type: ProtocolData.order_type.PARTIAL_RESTRICTED, + counter: '0', + zone_address: '0x1004f9615e79462c711ff05a386bdba91a762822', + seaport_address: '0x7d117aa8bd6d31c4fa91722f246388f38ab19482', + seaport_version: '1.5', + }, + salt: '0x3217c152146bf9f5', + signature: '0xf1522af4913159cdf1172d1c1bd511a3ca617f6c1d0b0ed588b2ce27618a2ac832c31ed4ee6ab2cea8af0efc2ad522468aa1c8c206291f4b343239acfea0a75e1b', + start_at: '2024-06-18T06:16:14Z', + status: { + name: 'ACTIVE', + }, + updated_at: '2024-06-18T06:16:59.006679Z', + }; + + // Merge the overrides with the default order + return { ...defaultOrder, ...overrides }; + } + + it('should return the remaining fillable units for ERC1155 type when amountToFill is not provided', () => { + const orderInput = createOrder({ + fill_status: { + numerator: '40', + denominator: '100', + }, + }); + + const result = determineFillableUnits(orderInput); + expect(result).to.equal('60'); // (100 - 40) * 100 / 100 + }); + + it('should return the original offer amount if order is unfilled i.e numerator or denominator is 0', () => { + const order: Order = createOrder(); + + const result = determineFillableUnits(order); + expect(result).to.equal('100'); + }); + + it('should return amountToFill if provided', () => { + const order: Order = createOrder({ + fill_status: { + numerator: '40', + denominator: '100', + }, + }); + + const amountToFill = '50'; + const result = determineFillableUnits(order, amountToFill); + expect(result).to.equal(amountToFill); + }); + + it('should return undefined if order type is not ERC1155', () => { + const order: Order = createOrder({ + sell: [ + { + type: 'ERC721', + contract_address: '0xEEb7Da6De152597830eD16361633e362A2F59410', + token_id: '123', + }, + ], + fill_status: { + numerator: '0', + denominator: '0', + }, + }); + + const result = determineFillableUnits(order); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(result).to.be.undefined; + }); +}); diff --git a/packages/orderbook/src/seaport/fillable-units.ts b/packages/orderbook/src/seaport/fillable-units.ts new file mode 100644 index 0000000000..47c52aab8b --- /dev/null +++ b/packages/orderbook/src/seaport/fillable-units.ts @@ -0,0 +1,22 @@ +import { Order } from 'openapi/sdk'; + +export function determineFillableUnits(order: Order, amountToFill?: string): string | undefined { + if (order.sell[0].type === 'ERC1155' && !amountToFill) { + // fill status is expressed as a ratio + const { numerator, denominator } = order.fill_status; + const originalOfferAmt = BigInt(order.sell[0].amount); + + if (numerator === '0' || denominator === '0') { + return originalOfferAmt.toString(); + } + + // calculate the remaining amount to fill + // remaining = ((denominator - numerator) * originalOfferAmt) / denominator + const remaining = ((BigInt(denominator) - BigInt(numerator)) * BigInt(originalOfferAmt)) + / BigInt(denominator); + + return remaining.toString(); + } + + return amountToFill; +} diff --git a/packages/orderbook/src/seaport/seaport.ts b/packages/orderbook/src/seaport/seaport.ts index 4437f56574..a52477caa2 100644 --- a/packages/orderbook/src/seaport/seaport.ts +++ b/packages/orderbook/src/seaport/seaport.ts @@ -36,6 +36,7 @@ import { getBulkOrderComponentsFromMessage, getOrderComponentsFromMessage } from import { SeaportLibFactory } from './seaport-lib-factory'; import { prepareTransaction } from './transaction'; import { mapImmutableOrderToSeaportOrderComponents } from './map-to-seaport-order'; +import { determineFillableUnits } from './fillable-units'; export class Seaport { constructor( @@ -175,7 +176,7 @@ export class Seaport { parameters: orderComponents, signature: order.signature, }, - unitsToFill, + unitsToFill: determineFillableUnits(order, unitsToFill), extraData, tips, }, @@ -244,7 +245,7 @@ export class Seaport { parameters: orderComponents, signature: o.order.signature, }, - unitsToFill: o.unitsToFill, + unitsToFill: determineFillableUnits(o.order, o.unitsToFill), extraData: o.extraData, tips, }; diff --git a/tests/func-tests/zkevm/features/order.feature b/tests/func-tests/zkevm/features/order.feature index 122aece59a..7ce1f65fc9 100644 --- a/tests/func-tests/zkevm/features/order.feature +++ b/tests/func-tests/zkevm/features/order.feature @@ -67,3 +67,30 @@ Feature: orderbook Then the listing should be of status active # Assert only the ERC1155 trade in this scenario And 1 trade should be available + + Scenario: create and fully fill a ERC1155 listing without an explicit fulfill amount + Given I have a funded offerer account + And the offerer account has 100 ERC1155 tokens + And I have a funded fulfiller account + When I create a listing to sell 100 ERC1155 tokens + Then the listing should be of status active + When I fulfill the listing to buy tokens + Then the listing should be of status filled + And 100 ERC1155 tokens should be transferred to the fulfiller + And 1 trade should be available + + Scenario: create and partially fill a ERC1155 listing, second fill without explicit amount + Given I have a funded offerer account + And the offerer account has 100 ERC1155 tokens + And I have a funded fulfiller account + When I create a listing to sell 100 ERC1155 tokens + Then the listing should be of status active + When I fulfill the listing to buy 90 tokens + Then the listing should be of status active + And 90 ERC1155 tokens should be transferred to the fulfiller + And 1 trade should be available + When I fulfill the listing to buy tokens + Then the listing should be of status filled + # Checks for the total amount of tokens transferred - 100 = 90 from first fulfilment + 10 from second fulfilment + And 100 ERC1155 tokens should be transferred to the fulfiller + And 2 trades should be available \ No newline at end of file diff --git a/tests/func-tests/zkevm/step-definitions/order.steps.ts b/tests/func-tests/zkevm/step-definitions/order.steps.ts index 4c94cffa84..16a1b46727 100644 --- a/tests/func-tests/zkevm/step-definitions/order.steps.ts +++ b/tests/func-tests/zkevm/step-definitions/order.steps.ts @@ -13,7 +13,7 @@ import { givenIHaveAFundedOffererAccount, thenTheListingShouldBeOfStatus, whenICreateAListing, whenIFulfillTheListingToBuy, andERC1155TokensShouldBeTransferredToTheFulfiller, thenTheListingsShouldBeOfStatus, - whenIFulfillBulkListings, + whenIFulfillBulkListings, whenIFulfillTheListingToBuyWithoutExplicitFulfillmentAmt, whenICreateABulkListing, } from './shared'; @@ -257,4 +257,89 @@ defineFeature(feature, (test) => { andTradeShouldBeAvailable(and, sdk, fulfiller, getERC1155ListingId); }, 120_000); + + test('create and fully fill a ERC1155 listing without an explicit fulfill amount', ({ + given, + when, + then, + and, + }) => { + const offerer = new Wallet(Wallet.createRandom().privateKey, provider); + const fulfiller = new Wallet(Wallet.createRandom().privateKey, provider); + const testTokenId = getRandomTokenId(); + + let listingId: string = ''; + + // these callback functions are required to update / retrieve test level state variables from shared steps. + const setListingId = (id: string) => { + listingId = id; + }; + + const getListingId = () => listingId; + givenIHaveAFundedOffererAccount(given, bankerWallet, offerer); + + andTheOffererAccountHasERC1155Tokens(and, bankerWallet, offerer, erc1155ContractAddress, testTokenId); + + andIHaveAFundedFulfillerAccount(and, bankerWallet, fulfiller); + + whenICreateAListing(when, sdk, offerer, erc1155ContractAddress, testTokenId, setListingId); + + thenTheListingShouldBeOfStatus(then, sdk, getListingId); + + whenIFulfillTheListingToBuyWithoutExplicitFulfillmentAmt(when, sdk, fulfiller, getListingId); + + thenTheListingShouldBeOfStatus(then, sdk, getListingId); + + // eslint-disable-next-line max-len + andERC1155TokensShouldBeTransferredToTheFulfiller(and, bankerWallet, erc1155ContractAddress, testTokenId, fulfiller); + + andTradeShouldBeAvailable(and, sdk, fulfiller, getListingId); + }, 120_000); + + test('create and partially fill a ERC1155 listing, second fill without explicit amount', ({ + given, + when, + then, + and, + }) => { + const offerer = new Wallet(Wallet.createRandom().privateKey, provider); + const fulfiller = new Wallet(Wallet.createRandom().privateKey, provider); + const testTokenId = getRandomTokenId(); + + let listingId: string = ''; + + // these callback functions are required to update / retrieve test level state variables from shared steps. + const setListingId = (id: string) => { + listingId = id; + }; + + const getListingId = () => listingId; + givenIHaveAFundedOffererAccount(given, bankerWallet, offerer); + + andTheOffererAccountHasERC1155Tokens(and, bankerWallet, offerer, erc1155ContractAddress, testTokenId); + + andIHaveAFundedFulfillerAccount(and, bankerWallet, fulfiller); + + whenICreateAListing(when, sdk, offerer, erc1155ContractAddress, testTokenId, setListingId); + + thenTheListingShouldBeOfStatus(then, sdk, getListingId); + + whenIFulfillTheListingToBuy(when, sdk, fulfiller, getListingId); + + thenTheListingShouldBeOfStatus(then, sdk, getListingId); + + // eslint-disable-next-line max-len + andERC1155TokensShouldBeTransferredToTheFulfiller(and, bankerWallet, erc1155ContractAddress, testTokenId, fulfiller); + + andTradeShouldBeAvailable(and, sdk, fulfiller, getListingId); + + whenIFulfillTheListingToBuyWithoutExplicitFulfillmentAmt(when, sdk, fulfiller, getListingId); + + thenTheListingShouldBeOfStatus(then, sdk, getListingId); + + // eslint-disable-next-line max-len + andERC1155TokensShouldBeTransferredToTheFulfiller(and, bankerWallet, erc1155ContractAddress, testTokenId, fulfiller); + + andTradeShouldBeAvailable(and, sdk, fulfiller, getListingId); + }, 120_000); }); diff --git a/tests/func-tests/zkevm/step-definitions/shared.ts b/tests/func-tests/zkevm/step-definitions/shared.ts index b75e36d338..9380e4c31c 100644 --- a/tests/func-tests/zkevm/step-definitions/shared.ts +++ b/tests/func-tests/zkevm/step-definitions/shared.ts @@ -217,6 +217,18 @@ export const whenIFulfillTheListingToBuy = ( }); }; +export const whenIFulfillTheListingToBuyWithoutExplicitFulfillmentAmt = ( + when: DefineStepFunction, + sdk: orderbook.Orderbook, + fulfiller: Wallet, + getListingId: () => string, +) => { + when(/^I fulfill the listing to buy tokens?$/, async () => { + const listingId = getListingId(); + await fulfillListing(sdk, listingId, fulfiller); + }); +}; + export const whenIFulfillBulkListings = ( when: DefineStepFunction, sdk: orderbook.Orderbook, diff --git a/tests/func-tests/zkevm/yarn.lock b/tests/func-tests/zkevm/yarn.lock index d424a29198..dc83a5e3cb 100644 --- a/tests/func-tests/zkevm/yarn.lock +++ b/tests/func-tests/zkevm/yarn.lock @@ -1105,7 +1105,7 @@ __metadata: "@imtbl/sdk@file:../../../sdk::locator=func-tests-imx%40workspace%3A.": version: 0.0.0 - resolution: "@imtbl/sdk@file:../../../sdk#../../../sdk::hash=972221&locator=func-tests-imx%40workspace%3A." + resolution: "@imtbl/sdk@file:../../../sdk#../../../sdk::hash=27c191&locator=func-tests-imx%40workspace%3A." dependencies: "@0xsequence/abi": ^1.4.3 "@0xsequence/core": ^1.4.3 @@ -1162,7 +1162,7 @@ __metadata: optional: true prisma: optional: true - checksum: ff9a304bbe822028dd13295de657af04bcc19b4b5b0fd98fa49f624f05ece052041caaf690d33a495e7485dce9400c1eab48df0a3923b48534742697590f3364 + checksum: ece1b9f5eb071b687eb942e1885e5718dbec44ec6bd656b1dd7bffcee58f30f46d4dbc48ee0149a00d4b99ddb20af07c990f92e435b914e3ce39764bfba8072c languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index cbf52672b4..702b6185c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3748,6 +3748,7 @@ __metadata: "@typechain/ethers-v5": ^10.2.0 "@types/jest": ^29.4.3 axios: ^1.6.5 + chai: ^5.1.1 dotenv: ^16.0.3 eslint: ^8.40.0 ethers: ^5.7.2 @@ -9199,6 +9200,13 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^4.3.16": + version: 4.3.16 + resolution: "@types/chai@npm:4.3.16" + checksum: bb5f52d1b70534ed8b4bf74bd248add003ffe1156303802ea367331607c06b494da885ffbc2b674a66b4f90c9ee88759790a5f243879f6759f124f22328f5e95 + languageName: node + linkType: hard + "@types/connect-history-api-fallback@npm:^1.3.5": version: 1.5.0 resolution: "@types/connect-history-api-fallback@npm:1.5.0" @@ -11886,6 +11894,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 + languageName: node + linkType: hard + "ast-types-flow@npm:^0.0.7": version: 0.0.7 resolution: "ast-types-flow@npm:0.0.7" @@ -13092,6 +13107,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.1": + version: 5.1.1 + resolution: "chai@npm:5.1.1" + dependencies: + assertion-error: ^2.0.1 + check-error: ^2.1.1 + deep-eql: ^5.0.1 + loupe: ^3.1.0 + pathval: ^2.0.0 + checksum: 1e0a5e1b5febdfa8ceb97b9aff608286861ecb86533863119b2f39f07c08fb59f3c1791ab554947f009b9d71d509b9e4e734fb12133cb81f231c2c2ee7c1e738 + languageName: node + linkType: hard + "chalk@npm:4.1.1, chalk@npm:^4.1.1": version: 4.1.1 resolution: "chalk@npm:4.1.1" @@ -13168,6 +13196,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: d785ed17b1d4a4796b6e75c765a9a290098cf52ff9728ce0756e8ffd4293d2e419dd30c67200aee34202463b474306913f2fcfaf1890641026d9fc6966fea27a + languageName: node + linkType: hard + "check-more-types@npm:^2.24.0": version: 2.24.0 resolution: "check-more-types@npm:2.24.0" @@ -14632,6 +14667,13 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 6aaaadb4c19cbce42e26b2bbe5bd92875f599d2602635dc97f0294bae48da79e89470aedee05f449e0ca8c65e9fd7e7872624d1933a1db02713d99c2ca8d1f24 + languageName: node + linkType: hard + "deep-equal@npm:^2.0.5": version: 2.2.2 resolution: "deep-equal@npm:2.2.2" @@ -17432,6 +17474,13 @@ __metadata: languageName: node linkType: hard +"get-func-name@npm:^2.0.1": + version: 2.0.2 + resolution: "get-func-name@npm:2.0.2" + checksum: 3f62f4c23647de9d46e6f76d2b3eafe58933a9b3830c60669e4180d6c601ce1b4aa310ba8366143f55e52b139f992087a9f0647274e8745621fa2af7e0acf13b + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1": version: 1.2.1 resolution: "get-intrinsic@npm:1.2.1" @@ -21549,6 +21598,15 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.0": + version: 3.1.1 + resolution: "loupe@npm:3.1.1" + dependencies: + get-func-name: ^2.0.1 + checksum: c7efa6bc6d71f25ca03eb13c9a069e35ed86799e308ca27a7a3eff8cdf9500e7c22d1f2411468d154a8e960e91e5e685e0c6c83e96db748f177c1adf30811153 + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -23568,6 +23626,13 @@ __metadata: languageName: node linkType: hard +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: 682b6a6289de7990909effef7dae9aa7bb6218c0426727bccf66a35b34e7bfbc65615270c5e44e3c9557a5cb44b1b9ef47fc3cb18bce6ad3ba92bcd28467ed7d + languageName: node + linkType: hard + "pbkdf2@npm:^3.0.17, pbkdf2@npm:^3.0.3": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" @@ -28597,8 +28662,10 @@ __metadata: "@actions/core": ^1.10.1 "@emotion/react": ^11.11.3 "@release-it-plugins/workspaces": ^4.0.0 + "@types/chai": ^4.3.16 "@typescript-eslint/eslint-plugin": ^5.57.1 "@typescript-eslint/parser": ^5.57.1 + chai: ^5.1.1 eslint: ^8.40.0 eslint-config-airbnb: ^19.0.4 eslint-config-airbnb-typescript: ^17.0.0