diff --git a/contracts/accounts.js b/contracts/accounts.js index 0fc9eaa24..4e3c7662a 100644 --- a/contracts/accounts.js +++ b/contracts/accounts.js @@ -105,6 +105,12 @@ const ACCOUNT_18 = { "0x0492e7d36225b5998466345082a7ae3942b189792c5d348cb551ac4d91d67007" }; +const ACCOUNT_19 = { + address: "0xfCd5941c09BD0B5d33ee7296e6f11F28109746d1", + privateKey: + "0x77ee913bd97d7d31dc67188cc329bc604fe8ddf82c32168a2b5c7e37279d0537" +}; + const ACCOUNTS = [ ACCOUNT_1, ACCOUNT_2, @@ -123,7 +129,8 @@ const ACCOUNTS = [ ACCOUNT_15, ACCOUNT_16, ACCOUNT_17, - ACCOUNT_18 + ACCOUNT_18, + ACCOUNT_19 ]; module.exports = { @@ -145,5 +152,6 @@ module.exports = { ACCOUNT_15, ACCOUNT_16, ACCOUNT_17, - ACCOUNT_18 + ACCOUNT_18, + ACCOUNT_19 }; diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index 8f6d9a513..5f5202c7f 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -68,7 +68,7 @@ subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS, async (_, { config }) => { const accountsFromEnv = process.env.DEPLOYER_PK ? [process.env.DEPLOYER_PK] : []; - +console.log("HARDHAAAAAAT!", ACCOUNTS); /** * @type import('hardhat/config').HardhatUserConfig */ diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index a69a3a276..4beb4cfd6 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -5,7 +5,7 @@ services: context: ../. dockerfile: ./e2e/meta-tx-gateway/Dockerfile user: "node:node" - image: meta-tx-gateway:202309251200 + image: meta-tx-gateway:202310231200 ports: - "8888:8888" environment: @@ -20,7 +20,7 @@ services: hardhat-node: build: ../contracts - image: hardhat-node:8319ec72c2335a49b74aeefd232ea9f0a6dfd368_3 + image: hardhat-node:8319ec72c2335a49b74aeefd232ea9f0a6dfd368_4 ports: - "8545:8545" volumes: diff --git a/e2e/tests/core-sdk-funds.test.ts b/e2e/tests/core-sdk-funds.test.ts new file mode 100644 index 000000000..3c3f3458b --- /dev/null +++ b/e2e/tests/core-sdk-funds.test.ts @@ -0,0 +1,354 @@ +import { utils, constants, BigNumber } from "ethers"; +import { parseEther } from "@ethersproject/units"; +import { CoreSDK } from "../../packages/core-sdk/src"; +import { FundsEntityFieldsFragment } from "../../packages/core-sdk/src/subgraph"; + +import { + initCoreSDKWithFundedWallet, + ensureCreatedSeller, + ensureMintedAndAllowedTokens, + MOCK_ERC20_ADDRESS, + waitForGraphNodeIndexing, + seedWallet19, + createOffer, + initSellerAndBuyerSDKs, + commitToOffer, + mockErc20Contract +} from "./utils"; + +const seedWallet = seedWallet19; // be sure the seedWallet is not used by another test (to allow concurrent run) + +jest.setTimeout(800_000); + +describe("core-sdk-funds", () => { + describe("deposit funds", () => { + test("ETH", async () => { + const sellerFundsDepositInEth = "5"; + const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( + seedWallet + ); + const sellers = await ensureCreatedSeller(fundedWallet); + const [seller] = sellers; + + const funds = await depositFunds({ + coreSDK, + fundsDepositAmountInEth: sellerFundsDepositInEth, + sellerId: seller.id + }); + + expect(funds).toBeTruthy(); + expect(funds.availableAmount).toBe( + utils.parseEther(sellerFundsDepositInEth).toString() + ); + expect(funds.token.symbol.toUpperCase()).toBe("ETH"); + }); + + test("ERC20", async () => { + const sellerFundsDeposit = "5"; + const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( + seedWallet + ); + const sellers = await ensureCreatedSeller(fundedWallet); + const [seller] = sellers; + + await ensureMintedAndAllowedTokens([fundedWallet], sellerFundsDeposit); + + const funds = await depositFunds({ + coreSDK, + fundsDepositAmountInEth: sellerFundsDeposit, + sellerId: seller.id, + fundsTokenAddress: MOCK_ERC20_ADDRESS + }); + + expect(funds).toBeTruthy(); + expect(funds.availableAmount).toBe( + utils.parseEther(sellerFundsDeposit).toString() + ); + expect(funds.token.symbol.toUpperCase()).toBe("20TEST"); + }); + }); + + describe("withdraw funds", () => { + test("ETH", async () => { + const sellerFundsDepositInEth = "5"; + const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( + seedWallet + ); + const sellers = await ensureCreatedSeller(fundedWallet); + const [seller] = sellers; + + const funds = await depositFunds({ + coreSDK, + fundsDepositAmountInEth: sellerFundsDepositInEth, + sellerId: seller.id + }); + expect(funds.availableAmount).toEqual( + utils.parseEther(sellerFundsDepositInEth).toString() + ); + + const tokenAddress = funds.token.address; + + const updatedFunds = await withdrawFunds({ + coreSDK, + sellerId: seller.id, + tokenAddresses: [tokenAddress], + amountsInEth: [sellerFundsDepositInEth] + }); + + expect(updatedFunds[0].availableAmount).toEqual("0"); + }); + + test("ETH and ERC20", async () => { + const sellerFundsDepositInEth = "5"; + const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( + seedWallet + ); + const sellers = await ensureCreatedSeller(fundedWallet); + const [seller] = sellers; + + const ethFunds = await depositFunds({ + coreSDK, + fundsDepositAmountInEth: sellerFundsDepositInEth, + sellerId: seller.id, + fundsTokenAddress: constants.AddressZero + }); + expect(ethFunds.availableAmount).toEqual( + utils.parseEther(sellerFundsDepositInEth).toString() + ); + + await ensureMintedAndAllowedTokens( + [fundedWallet], + sellerFundsDepositInEth + ); + + const mockErc20Funds = await depositFunds({ + coreSDK, + fundsDepositAmountInEth: sellerFundsDepositInEth, + sellerId: seller.id, + fundsTokenAddress: MOCK_ERC20_ADDRESS + }); + + expect(mockErc20Funds.availableAmount).toEqual( + utils.parseEther(sellerFundsDepositInEth).toString() + ); + + const updatedFunds = await withdrawFunds({ + coreSDK, + sellerId: seller.id, + tokenAddresses: [constants.AddressZero, MOCK_ERC20_ADDRESS], + amountsInEth: [sellerFundsDepositInEth, sellerFundsDepositInEth] + }); + + const ethFundsAvailable = updatedFunds.find( + (fund) => fund.token.address === constants.AddressZero + )?.availableAmount; + const mockErc20FundsAvailable = updatedFunds.find( + (fund) => + fund.token.address.toLowerCase() === MOCK_ERC20_ADDRESS.toLowerCase() + )?.availableAmount; + expect(ethFundsAvailable).toEqual("0"); + expect(mockErc20FundsAvailable).toEqual("0"); + }); + }); + + describe("locked funds", () => { + test("completed exchanges should be excluded when coputing the locked funds value", async () => { + const { sellerCoreSDK, buyerCoreSDK, sellerWallet, buyerWallet } = + await initSellerAndBuyerSDKs(seedWallet); + const sellers = await ensureCreatedSeller(sellerWallet); + + const tokenAddress = mockErc20Contract.address; + const getErc20Balance = async (account: string) => + await sellerCoreSDK.erc20BalanceOf({ + contractAddress: tokenAddress, + owner: account + }); + + const price = parseEther("1"); + const sellerDeposit = parseEther("0.1"); + const buyerCancelPenalty = parseEther("0.01"); + const protocolFee = parseEther("0.005"); + const quantityAvailable = 5; + await ensureMintedAndAllowedTokens([sellerWallet], 100); + await ensureMintedAndAllowedTokens([buyerWallet], 100); + + const [seller] = sellers; + + await ( + await sellerCoreSDK.depositFunds( + seller.id, + sellerDeposit.mul(quantityAvailable), + tokenAddress + ) + ).wait(); + const offer1 = await createOffer(sellerCoreSDK, { + quantityAvailable, + price, + sellerDeposit, + buyerCancelPenalty, + exchangeToken: tokenAddress + }); + + const buyerErc20BalanceBefore = await getErc20Balance( + buyerWallet.address + ); + + const ex1 = await commitToOffer({ + offerId: offer1.id, + buyerCoreSDK, + sellerCoreSDK + }); + await waitForGraphNodeIndexing(); + const [buyer] = await buyerCoreSDK.getBuyers({ + buyersFilter: { + wallet: buyerWallet.address + } + }); + const ex2 = await commitToOffer({ + offerId: offer1.id, + buyerCoreSDK, + sellerCoreSDK + }); + await waitForGraphNodeIndexing(); + const ex3 = await commitToOffer({ + offerId: offer1.id, + buyerCoreSDK, + sellerCoreSDK + }); + await waitForGraphNodeIndexing(); + + const ex4 = await commitToOffer({ + offerId: offer1.id, + buyerCoreSDK, + sellerCoreSDK + }); + await waitForGraphNodeIndexing(); + + const ex5 = await commitToOffer({ + offerId: offer1.id, + buyerCoreSDK, + sellerCoreSDK + }); + + await (await buyerCoreSDK.cancelVoucher(ex1.id)).wait(); + await (await buyerCoreSDK.redeemVoucher(ex2.id)).wait(); + await (await buyerCoreSDK.redeemVoucher(ex3.id)).wait(); + await (await buyerCoreSDK.raiseDispute(ex3.id)).wait(); + await (await buyerCoreSDK.redeemVoucher(ex4.id)).wait(); + await (await buyerCoreSDK.completeExchange(ex4.id)).wait(); + await (await sellerCoreSDK.revokeVoucher(ex5.id)).wait(); + await waitForGraphNodeIndexing(); + + const sellerFundsAfter = await sellerCoreSDK.getFunds({ + fundsFilter: { + account: seller.id + } + }); + const buyerFundsAfter = await sellerCoreSDK.getFunds({ + fundsFilter: { + account: buyer.id + } + }); + const [firstSeller] = await sellerCoreSDK.getSellers({ + sellersFilter: { + id: seller.id + }, + includeExchanges: true + }); + const offers = await sellerCoreSDK.getExchanges({ + exchangesFilter: { + id_in: firstSeller.exchanges + ?.filter((e) => !e.finalizedDate) + .map((e) => e.id) + } + }); + const lockedSellerDeposit = offers + .map((o) => o.offer.sellerDeposit) + .reduce((prev, current) => prev.add(current), BigNumber.from(0)); + const lockedBuyerPrice = offers + .map((o) => o.offer.price) + .reduce((prev, current) => prev.add(current), BigNumber.from(0)); + + expect(sellerFundsAfter[0].availableAmount).toBe( + sellerDeposit + .mul(quantityAvailable) // initial deposit + .add(buyerCancelPenalty) // cancel ex1 + .sub(sellerDeposit) // commit ex2 + .sub(sellerDeposit) // commit ex3 + .add(price.sub(protocolFee)) // ex4 complete + .sub(sellerDeposit) + .toString() + ); + expect(await getErc20Balance(buyerWallet.address)).toBe( + BigNumber.from(buyerErc20BalanceBefore) + .sub(price.mul(quantityAvailable)) // commits + .toString() + ); + expect(buyerFundsAfter[0].availableAmount).toBe( + price + .sub(buyerCancelPenalty) // cancel ex1 + .add(price.add(sellerDeposit)) // ex5 revoke + .toString() + ); + expect(lockedSellerDeposit.toString()).toBe( + sellerDeposit.add(sellerDeposit).toString() // ex2 & ex3 + ); + expect(lockedBuyerPrice.toString()).toBe( + price.add(price).toString() // ex2 & ex3 + ); + }); + }); +}); + +async function depositFunds(args: { + coreSDK: CoreSDK; + sellerId: string; + fundsDepositAmountInEth?: string; + fundsTokenAddress?: string; +}): Promise { + const tokenAddress = args.fundsTokenAddress ?? constants.AddressZero; + const depositFundsTxResponse = await args.coreSDK.depositFunds( + args.sellerId, + utils.parseEther(args.fundsDepositAmountInEth || "5"), + tokenAddress + ); + await depositFundsTxResponse.wait(); + + await waitForGraphNodeIndexing(); + + const funds = await args.coreSDK.getFunds({ + fundsFilter: { + accountId: args.sellerId + } + }); + + const depositedFunds = funds.find( + (x) => x.token.address.toLowerCase() === tokenAddress.toLowerCase() + ); + if (!depositedFunds) throw new Error(`No funds found for ${tokenAddress}`); + + return depositedFunds; +} + +async function withdrawFunds(args: { + coreSDK: CoreSDK; + sellerId: string; + tokenAddresses: Array; + amountsInEth: Array; +}): Promise> { + const withdrawResponse = await args.coreSDK.withdrawFunds( + args.sellerId, + args.tokenAddresses, + args.amountsInEth.map((amount) => utils.parseEther(amount)) + ); + await withdrawResponse.wait(); + await waitForGraphNodeIndexing(); + + const funds = await args.coreSDK.getFunds({ + fundsFilter: { + accountId: args.sellerId + } + }); + + return funds; +} diff --git a/e2e/tests/core-sdk.test.ts b/e2e/tests/core-sdk.test.ts index f7e3c7228..919bad59d 100644 --- a/e2e/tests/core-sdk.test.ts +++ b/e2e/tests/core-sdk.test.ts @@ -33,7 +33,8 @@ import { createOfferWithCondition, createSellerAndOfferWithCondition, createSeller, - createSellerAndOffer + createSellerAndOffer, + commitToOffer } from "./utils"; import { EvaluationMethod, GatingType, TokenType } from "@bosonprotocol/common"; @@ -94,53 +95,6 @@ describe("core-sdk", () => { ).toBe(true); }); - describe("deposit funds", () => { - test("ETH", async () => { - const sellerFundsDepositInEth = "5"; - const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( - seedWallet - ); - const sellers = await ensureCreatedSeller(fundedWallet); - const [seller] = sellers; - - const funds = await depositFunds({ - coreSDK, - fundsDepositAmountInEth: sellerFundsDepositInEth, - sellerId: seller.id - }); - - expect(funds).toBeTruthy(); - expect(funds.availableAmount).toBe( - utils.parseEther(sellerFundsDepositInEth).toString() - ); - expect(funds.token.symbol.toUpperCase()).toBe("ETH"); - }); - - test("ERC20", async () => { - const sellerFundsDeposit = "5"; - const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( - seedWallet - ); - const sellers = await ensureCreatedSeller(fundedWallet); - const [seller] = sellers; - - await ensureMintedAndAllowedTokens([fundedWallet], sellerFundsDeposit); - - const funds = await depositFunds({ - coreSDK, - fundsDepositAmountInEth: sellerFundsDeposit, - sellerId: seller.id, - fundsTokenAddress: MOCK_ERC20_ADDRESS - }); - - expect(funds).toBeTruthy(); - expect(funds.availableAmount).toBe( - utils.parseEther(sellerFundsDeposit).toString() - ); - expect(funds.token.symbol.toUpperCase()).toBe("20TEST"); - }); - }); - test("void offer", async () => { const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( seedWallet @@ -1138,90 +1092,6 @@ describe("core-sdk", () => { expect(exchangesAfterComplete[1].completedDate).toBeTruthy(); }); - describe("withdraw funds", () => { - test("ETH", async () => { - const sellerFundsDepositInEth = "5"; - const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( - seedWallet - ); - const sellers = await ensureCreatedSeller(fundedWallet); - const [seller] = sellers; - - const funds = await depositFunds({ - coreSDK, - fundsDepositAmountInEth: sellerFundsDepositInEth, - sellerId: seller.id - }); - expect(funds.availableAmount).toEqual( - utils.parseEther(sellerFundsDepositInEth).toString() - ); - - const tokenAddress = funds.token.address; - - const updatedFunds = await withdrawFunds({ - coreSDK, - sellerId: seller.id, - tokenAddresses: [tokenAddress], - amountsInEth: [sellerFundsDepositInEth] - }); - - expect(updatedFunds[0].availableAmount).toEqual("0"); - }); - - test("ETH and ERC20", async () => { - const sellerFundsDepositInEth = "5"; - const { coreSDK, fundedWallet } = await initCoreSDKWithFundedWallet( - seedWallet - ); - const sellers = await ensureCreatedSeller(fundedWallet); - const [seller] = sellers; - - const ethFunds = await depositFunds({ - coreSDK, - fundsDepositAmountInEth: sellerFundsDepositInEth, - sellerId: seller.id, - fundsTokenAddress: constants.AddressZero - }); - expect(ethFunds.availableAmount).toEqual( - utils.parseEther(sellerFundsDepositInEth).toString() - ); - - await ensureMintedAndAllowedTokens( - [fundedWallet], - sellerFundsDepositInEth - ); - - const mockErc20Funds = await depositFunds({ - coreSDK, - fundsDepositAmountInEth: sellerFundsDepositInEth, - sellerId: seller.id, - fundsTokenAddress: MOCK_ERC20_ADDRESS - }); - - expect(mockErc20Funds.availableAmount).toEqual( - utils.parseEther(sellerFundsDepositInEth).toString() - ); - - const updatedFunds = await withdrawFunds({ - coreSDK, - sellerId: seller.id, - tokenAddresses: [constants.AddressZero, MOCK_ERC20_ADDRESS], - amountsInEth: [sellerFundsDepositInEth, sellerFundsDepositInEth] - }); - - const ethFundsAvailable = updatedFunds.find( - (fund) => fund.token.address === constants.AddressZero - )?.availableAmount; - const mockErc20FundsAvailable = updatedFunds.find( - (fund) => - fund.token.address.toLowerCase() === - MOCK_ERC20_ADDRESS.toLowerCase() - )?.availableAmount; - expect(ethFundsAvailable).toEqual("0"); - expect(mockErc20FundsAvailable).toEqual("0"); - }); - }); - describe("disputes", () => { let exchange: ExchangeFieldsFragment; const sellerWallet = sellerWallet2; @@ -1467,48 +1337,6 @@ async function depositFunds(args: { return depositedFunds; } -async function withdrawFunds(args: { - coreSDK: CoreSDK; - sellerId: string; - tokenAddresses: Array; - amountsInEth: Array; -}): Promise> { - const withdrawResponse = await args.coreSDK.withdrawFunds( - args.sellerId, - args.tokenAddresses, - args.amountsInEth.map((amount) => utils.parseEther(amount)) - ); - await withdrawResponse.wait(); - await waitForGraphNodeIndexing(); - - const funds = await args.coreSDK.getFunds({ - fundsFilter: { - accountId: args.sellerId - } - }); - - return funds; -} - -async function commitToOffer(args: { - buyerCoreSDK: CoreSDK; - sellerCoreSDK: CoreSDK; - offerId: BigNumberish; -}) { - const commitToOfferTxResponse = await args.buyerCoreSDK.commitToOffer( - args.offerId - ); - const commitToOfferTxReceipt = await commitToOfferTxResponse.wait(); - const exchangeId = args.buyerCoreSDK.getCommittedExchangeIdFromLogs( - commitToOfferTxReceipt.logs - ); - await waitForGraphNodeIndexing(); - const exchange = await args.sellerCoreSDK.getExchangeById( - exchangeId as string - ); - return exchange; -} - async function commitToConditionalOffer(args: { buyerCoreSDK: CoreSDK; sellerCoreSDK: CoreSDK; diff --git a/e2e/tests/utils.ts b/e2e/tests/utils.ts index d56e95c0f..a1f470bda 100644 --- a/e2e/tests/utils.ts +++ b/e2e/tests/utils.ts @@ -36,7 +36,8 @@ import { ACCOUNT_15, ACCOUNT_16, ACCOUNT_17, - ACCOUNT_18 + ACCOUNT_18, + ACCOUNT_19 } from "../../contracts/accounts"; import { MOCK_ERC1155_ABI, @@ -152,6 +153,8 @@ export const seedWallet16 = new Wallet(ACCOUNT_16.privateKey, provider); export const seedWallet17 = new Wallet(ACCOUNT_17.privateKey, provider); // seedWallets used by core-sdk-set-contract-uri export const seedWallet18 = new Wallet(ACCOUNT_18.privateKey, provider); +// seedWallets used by core-sdk-set-contract-uri +export const seedWallet19 = new Wallet(ACCOUNT_19.privateKey, provider); export const mockErc20Contract = new Contract( MOCK_ERC20_ADDRESS, @@ -729,3 +732,23 @@ export function createSeaportOrder(args: { signature: "0x" // no signature required if the transaction is sent by the offerer }; } + +export async function commitToOffer(args: { + buyerCoreSDK: CoreSDK; + sellerCoreSDK: CoreSDK; + offerId: BigNumberish; +}) { + const commitToOfferTxResponse = await args.buyerCoreSDK.commitToOffer( + args.offerId + ); + const commitToOfferTxReceipt = await commitToOfferTxResponse.wait(); + const exchangeId = args.buyerCoreSDK.getCommittedExchangeIdFromLogs( + commitToOfferTxReceipt.logs + ); + if (!exchangeId) { + throw new Error("exchangeId is not defined"); + } + await waitForGraphNodeIndexing(); + const exchange = await args.buyerCoreSDK.getExchangeById(exchangeId); + return exchange; +} diff --git a/packages/subgraph/src/mappings/exchange-handler.ts b/packages/subgraph/src/mappings/exchange-handler.ts index 46bfe4340..779db76d4 100644 --- a/packages/subgraph/src/mappings/exchange-handler.ts +++ b/packages/subgraph/src/mappings/exchange-handler.ts @@ -69,6 +69,7 @@ export function handleVoucherRevokedEvent(event: VoucherRevoked): void { if (exchange) { exchange.state = "REVOKED"; exchange.revokedDate = event.block.timestamp; + exchange.finalizedDate = event.block.timestamp; exchange.save(); saveExchangeEventLogs( @@ -91,6 +92,7 @@ export function handleVoucherExpiredEvent(event: VoucherExpired): void { exchange.state = "CANCELLED"; exchange.expired = true; exchange.cancelledDate = event.block.timestamp; + exchange.finalizedDate = event.block.timestamp; exchange.save(); saveExchangeEventLogs( @@ -132,6 +134,7 @@ export function handleVoucherCanceledEvent(event: VoucherCanceled): void { if (exchange) { exchange.state = "CANCELLED"; exchange.cancelledDate = event.block.timestamp; + exchange.finalizedDate = event.block.timestamp; exchange.save(); saveExchangeEventLogs( @@ -215,6 +218,7 @@ export function handleExchangeCompletedEvent(event: ExchangeCompleted): void { if (exchange) { exchange.state = "COMPLETED"; exchange.completedDate = event.block.timestamp; + exchange.finalizedDate = event.block.timestamp; exchange.save(); saveExchangeEventLogs(