From 0c5609b550ef7819852bc81fcf2b3d5ddc81ef31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:57:22 +0000 Subject: [PATCH 1/5] Initial plan From 52848ca34e7f50d4c9219d42509b1b72ecdbf567 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:20:08 +0000 Subject: [PATCH 2/5] Add signMetaTxCommitToBuyerOffer to meta-tx handler, mixin, and e2e tests Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> --- e2e/tests/meta-tx.test.ts | 104 ++++++++++++++++++++++- packages/core-sdk/src/meta-tx/handler.ts | 22 ++++- packages/core-sdk/src/meta-tx/mixin.ts | 19 +++++ 3 files changed, 141 insertions(+), 4 deletions(-) diff --git a/e2e/tests/meta-tx.test.ts b/e2e/tests/meta-tx.test.ts index 2bbcc686f..0299155b3 100644 --- a/e2e/tests/meta-tx.test.ts +++ b/e2e/tests/meta-tx.test.ts @@ -1,5 +1,6 @@ import { ZERO_ADDRESS } from "./../../packages/core-sdk/tests/mocks"; import { BigNumberish } from "@ethersproject/bignumber"; +import { parseEther } from "@ethersproject/units"; import { Wallet, BigNumber, constants } from "ethers"; import { OfferFieldsFragment } from "../../packages/core-sdk/src/subgraph"; import { mockCreateOfferArgs } from "../../packages/common/tests/mocks"; @@ -26,12 +27,19 @@ import { createOfferWithCondition, getCollectionMetadataUri, createRandomWallet, - META_TX_API_ID_VOUCHER + META_TX_API_ID_VOUCHER, + createDisputeResolver, + deployerWallet, + initSellerAndBuyerSDKs } from "./utils"; import { CoreSDK, forwarder } from "../../packages/core-sdk/src"; import EvaluationMethod from "../../contracts/protocol-contracts/scripts/domain/EvaluationMethod"; import TokenType from "../../contracts/protocol-contracts/scripts/domain/TokenType"; -import { AuthTokenType, GatingType } from "../../packages/common"; +import { AuthTokenType, GatingType, OfferCreator } from "../../packages/common"; +import { + MSEC_PER_DAY, + MSEC_PER_SEC +} from "./../../packages/common/src/utils/timestamp"; const sellerWallet = seedWallet7; // be sure the seedWallet is not used by another test (to allow concurrent run) const sellerAddress = sellerWallet.address; @@ -724,6 +732,98 @@ describe("meta-tx", () => { }); }); + describe("#signMetaTxCommitToBuyerOffer()", () => { + test("native exchange token buyer-initiated offer", async () => { + const exchangeToken = constants.AddressZero; + const drFeeAmount = parseEther("0.005"); + + // Create a dispute resolver with a fee amount + const { fundedWallet: drFundedWallet } = + await initCoreSDKWithFundedWallet(sellerWallet); + const drAddress = drFundedWallet.address.toLowerCase(); + const { disputeResolver } = await createDisputeResolver( + drFundedWallet, + deployerWallet, + { + assistant: drAddress, + admin: drAddress, + treasury: drAddress, + metadataUri: "", + escalationResponsePeriodInMS: + 90 * MSEC_PER_DAY - 1 * MSEC_PER_SEC, + fees: [ + { + feeAmount: drFeeAmount, + tokenAddress: exchangeToken, + tokenName: "Native" + } + ], + sellerAllowList: [] + } + ); + + // Create fresh buyer/seller wallets + const { + sellerCoreSDK: sellerCoreSDKBuyer, + buyerCoreSDK: buyerCoreSDKBuyer, + sellerWallet: sellerFundedWallet + } = await initSellerAndBuyerSDKs(sellerWallet); + + // Buyer creates a buyer-initiated offer + const buyerInitiatedOffer = await createOffer(buyerCoreSDKBuyer, { + creator: OfferCreator.Buyer, + quantityAvailable: 1, + disputeResolverId: disputeResolver.id, + exchangeToken + }); + + // Buyer deposits offer.price to allow the seller to commit + const buyerDepositTx = await buyerCoreSDKBuyer.depositFunds( + buyerInitiatedOffer.buyerId, + buyerInitiatedOffer.price, + exchangeToken + ); + await buyerCoreSDKBuyer.waitForGraphNodeIndexing(buyerDepositTx); + + // Seller creates a seller account + const seller = await createSeller( + sellerCoreSDKBuyer, + sellerFundedWallet.address + ); + + // Seller deposits DR fee amount + const sellerDepositTx = await sellerCoreSDKBuyer.depositFunds( + seller.id, + drFeeAmount, + exchangeToken + ); + await sellerCoreSDKBuyer.waitForGraphNodeIndexing(sellerDepositTx); + + const nonce = Date.now(); + + // Seller signs meta tx for commitToBuyerOffer + const { r, s, v, functionName, functionSignature } = + await sellerCoreSDKBuyer.signMetaTxCommitToBuyerOffer({ + offerId: buyerInitiatedOffer.id, + sellerParams: {}, + nonce + }); + + // Relayer executes meta tx on behalf of seller + const metaTx = await sellerCoreSDKBuyer.relayMetaTransaction({ + functionName, + functionSignature, + nonce, + sigR: r, + sigS: s, + sigV: v + }); + const metaTxReceipt = await metaTx.wait(); + expect(metaTxReceipt.transactionHash).toBeTruthy(); + expect(BigNumber.from(metaTxReceipt.effectiveGasPrice).gt(0)).toBe(true); + }); + }); + describe("#signMetaTxRedeemVoucher()", () => { test("non-native exchange token offer", async () => { const commitTx = await buyerCoreSDK.commitToOffer(offerToCommit.id); diff --git a/packages/core-sdk/src/meta-tx/handler.ts b/packages/core-sdk/src/meta-tx/handler.ts index a632ead0d..74868df69 100644 --- a/packages/core-sdk/src/meta-tx/handler.ts +++ b/packages/core-sdk/src/meta-tx/handler.ts @@ -11,7 +11,8 @@ import { UpdateSellerArgs, OptInToSellerUpdateArgs, envConfigs, - abis + abis, + SellerOfferArgs } from "@bosonprotocol/common"; import { storeMetadataOnTheGraph } from "../offers/storage"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; @@ -24,7 +25,8 @@ import { } from "../accounts/interface"; import { bosonExchangeCommitHandlerIface, - bosonExchangeHandlerIface + bosonExchangeHandlerIface, + encodeCommitToBuyerOffer } from "../exchanges/interface"; import { bosonOfferHandlerIface, @@ -944,6 +946,22 @@ export async function signMetaTxCommitToConditionalOffer( }; } +export async function signMetaTxCommitToBuyerOffer( + args: BaseMetaTxArgs & { + offerId: BigNumberish; + sellerParams: SellerOfferArgs; + } +): Promise { + const functionName = + "commitToBuyerOffer(uint256,(uint256,(address[],uint256[]),address))"; + + return signMetaTx({ + ...args, + functionName, + functionSignature: encodeCommitToBuyerOffer(args.offerId, args.sellerParams) + }); +} + export async function signMetaTxCancelVoucher( args: BaseMetaTxArgs & { exchangeId: BigNumberish; diff --git a/packages/core-sdk/src/meta-tx/mixin.ts b/packages/core-sdk/src/meta-tx/mixin.ts index ee106ee17..3464de6a9 100644 --- a/packages/core-sdk/src/meta-tx/mixin.ts +++ b/packages/core-sdk/src/meta-tx/mixin.ts @@ -634,6 +634,25 @@ export class MetaTxMixin extends BaseCoreSDK { }); } + /** + * Encodes and signs a meta transaction for `commitToBuyerOffer` that can be relayed. + * @param args - Meta transaction args. + * @returns Signature. + */ + public async signMetaTxCommitToBuyerOffer( + args: Omit< + Parameters[0], + "web3Lib" | "metaTxHandlerAddress" | "chainId" + > + ) { + return handler.signMetaTxCommitToBuyerOffer({ + web3Lib: this._web3Lib, + metaTxHandlerAddress: this._protocolDiamond, + chainId: this._chainId, + ...args + }); + } + /** * Encodes and signs a meta transaction for `cancelVoucher` that can be relayed. * @param args - Meta transaction args. From 885d8dff4f904f795ae7bb0dce99e28f5325316a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:29:51 +0000 Subject: [PATCH 3/5] fix: set sellerDeposit=0 and drFeeAmount=0 in signMetaTxCommitToBuyerOffer e2e test Meta-tx relayers cannot forward ETH value, so commitToBuyerOffer with a non-zero sellerDeposit would fail with InsufficientValueReceived(). Set both sellerDeposit and drFeeAmount to 0 so no ETH transfer is needed. Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> --- e2e/tests/meta-tx.test.ts | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/e2e/tests/meta-tx.test.ts b/e2e/tests/meta-tx.test.ts index 0299155b3..9efa77adc 100644 --- a/e2e/tests/meta-tx.test.ts +++ b/e2e/tests/meta-tx.test.ts @@ -1,6 +1,5 @@ import { ZERO_ADDRESS } from "./../../packages/core-sdk/tests/mocks"; import { BigNumberish } from "@ethersproject/bignumber"; -import { parseEther } from "@ethersproject/units"; import { Wallet, BigNumber, constants } from "ethers"; import { OfferFieldsFragment } from "../../packages/core-sdk/src/subgraph"; import { mockCreateOfferArgs } from "../../packages/common/tests/mocks"; @@ -735,9 +734,11 @@ describe("meta-tx", () => { describe("#signMetaTxCommitToBuyerOffer()", () => { test("native exchange token buyer-initiated offer", async () => { const exchangeToken = constants.AddressZero; - const drFeeAmount = parseEther("0.005"); + // drFeeAmount must be 0 so the seller doesn't need to deposit funds before + // committing via meta-tx (meta-tx can't carry ETH value) + const drFeeAmount = "0"; - // Create a dispute resolver with a fee amount + // Create a dispute resolver with zero fee (required for meta-tx compatibility) const { fundedWallet: drFundedWallet } = await initCoreSDKWithFundedWallet(sellerWallet); const drAddress = drFundedWallet.address.toLowerCase(); @@ -769,12 +770,14 @@ describe("meta-tx", () => { sellerWallet: sellerFundedWallet } = await initSellerAndBuyerSDKs(sellerWallet); - // Buyer creates a buyer-initiated offer + // Buyer creates a buyer-initiated offer. + // sellerDeposit must be 0 so the meta-tx relayer doesn't need to forward ETH value. const buyerInitiatedOffer = await createOffer(buyerCoreSDKBuyer, { creator: OfferCreator.Buyer, quantityAvailable: 1, disputeResolverId: disputeResolver.id, - exchangeToken + exchangeToken, + sellerDeposit: "0" }); // Buyer deposits offer.price to allow the seller to commit @@ -786,18 +789,8 @@ describe("meta-tx", () => { await buyerCoreSDKBuyer.waitForGraphNodeIndexing(buyerDepositTx); // Seller creates a seller account - const seller = await createSeller( - sellerCoreSDKBuyer, - sellerFundedWallet.address - ); - - // Seller deposits DR fee amount - const sellerDepositTx = await sellerCoreSDKBuyer.depositFunds( - seller.id, - drFeeAmount, - exchangeToken - ); - await sellerCoreSDKBuyer.waitForGraphNodeIndexing(sellerDepositTx); + await createSeller(sellerCoreSDKBuyer, sellerFundedWallet.address); + // No seller depositFunds needed: drFeeAmount = 0 and sellerDeposit = 0 const nonce = Date.now(); From ec348e4337134ae0437465b7606a27a742242043 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:35:33 +0000 Subject: [PATCH 4/5] test: add non-native ERC20 token test case for signMetaTxCommitToBuyerOffer Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> --- e2e/tests/meta-tx.test.ts | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/e2e/tests/meta-tx.test.ts b/e2e/tests/meta-tx.test.ts index 9efa77adc..35d4dafc4 100644 --- a/e2e/tests/meta-tx.test.ts +++ b/e2e/tests/meta-tx.test.ts @@ -1,5 +1,6 @@ import { ZERO_ADDRESS } from "./../../packages/core-sdk/tests/mocks"; import { BigNumberish } from "@ethersproject/bignumber"; +import { parseEther } from "@ethersproject/units"; import { Wallet, BigNumber, constants } from "ethers"; import { OfferFieldsFragment } from "../../packages/core-sdk/src/subgraph"; import { mockCreateOfferArgs } from "../../packages/common/tests/mocks"; @@ -815,6 +816,104 @@ describe("meta-tx", () => { expect(metaTxReceipt.transactionHash).toBeTruthy(); expect(BigNumber.from(metaTxReceipt.effectiveGasPrice).gt(0)).toBe(true); }); + + test("non-native exchange token buyer-initiated offer", async () => { + const exchangeToken = MOCK_ERC20_ADDRESS; + const drFeeAmount = parseEther("0.001"); + + // Create a dispute resolver with an ERC20 fee + const { fundedWallet: drFundedWallet } = + await initCoreSDKWithFundedWallet(sellerWallet); + const drAddress = drFundedWallet.address.toLowerCase(); + const { disputeResolver } = await createDisputeResolver( + drFundedWallet, + deployerWallet, + { + assistant: drAddress, + admin: drAddress, + treasury: drAddress, + metadataUri: "", + escalationResponsePeriodInMS: + 90 * MSEC_PER_DAY - 1 * MSEC_PER_SEC, + fees: [ + { + feeAmount: drFeeAmount, + tokenAddress: exchangeToken, + tokenName: "ERC20" + } + ], + sellerAllowList: [] + } + ); + + // Create fresh buyer/seller wallets + const { + sellerCoreSDK: sellerCoreSDKBuyer, + buyerCoreSDK: buyerCoreSDKBuyer, + sellerWallet: sellerFundedWallet + } = await initSellerAndBuyerSDKs(sellerWallet); + + // Buyer creates a buyer-initiated offer with ERC20 exchange token. + // sellerDeposit is 0 to simplify the test setup. + const buyerInitiatedOffer = await createOffer(buyerCoreSDKBuyer, { + creator: OfferCreator.Buyer, + quantityAvailable: 1, + disputeResolverId: disputeResolver.id, + exchangeToken, + sellerDeposit: "0" + }); + + // Buyer approves ERC20 and deposits offer.price + await approveErc20Token( + buyerCoreSDKBuyer, + exchangeToken, + buyerInitiatedOffer.price + ); + const buyerDepositTx = await buyerCoreSDKBuyer.depositFunds( + buyerInitiatedOffer.buyerId, + buyerInitiatedOffer.price, + exchangeToken + ); + await buyerCoreSDKBuyer.waitForGraphNodeIndexing(buyerDepositTx); + + // Seller creates a seller account + const seller = await createSeller( + sellerCoreSDKBuyer, + sellerFundedWallet.address + ); + + // Seller approves ERC20 and deposits drFeeAmount into the protocol + await approveErc20Token(sellerCoreSDKBuyer, exchangeToken, drFeeAmount); + const sellerDepositTx = await sellerCoreSDKBuyer.depositFunds( + seller.id, + drFeeAmount, + exchangeToken + ); + await sellerCoreSDKBuyer.waitForGraphNodeIndexing(sellerDepositTx); + + const nonce = Date.now(); + + // Seller signs meta tx for commitToBuyerOffer + const { r, s, v, functionName, functionSignature } = + await sellerCoreSDKBuyer.signMetaTxCommitToBuyerOffer({ + offerId: buyerInitiatedOffer.id, + sellerParams: {}, + nonce + }); + + // Relayer executes meta tx on behalf of seller + const metaTx = await sellerCoreSDKBuyer.relayMetaTransaction({ + functionName, + functionSignature, + nonce, + sigR: r, + sigS: s, + sigV: v + }); + const metaTxReceipt = await metaTx.wait(); + expect(metaTxReceipt.transactionHash).toBeTruthy(); + expect(BigNumber.from(metaTxReceipt.effectiveGasPrice).gt(0)).toBe(true); + }); }); describe("#signMetaTxRedeemVoucher()", () => { From 72a683d337e792f989f9376032529832cebb2f73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:23:49 +0000 Subject: [PATCH 5/5] fix: mint ERC20 tokens to fresh wallets before depositFunds in non-native buyer offer test Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> --- e2e/tests/meta-tx.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/e2e/tests/meta-tx.test.ts b/e2e/tests/meta-tx.test.ts index 35d4dafc4..664c1d543 100644 --- a/e2e/tests/meta-tx.test.ts +++ b/e2e/tests/meta-tx.test.ts @@ -850,9 +850,17 @@ describe("meta-tx", () => { const { sellerCoreSDK: sellerCoreSDKBuyer, buyerCoreSDK: buyerCoreSDKBuyer, - sellerWallet: sellerFundedWallet + sellerWallet: sellerFundedWallet, + buyerWallet: buyerFundedWallet } = await initSellerAndBuyerSDKs(sellerWallet); + // Mint ERC20 tokens to fresh wallets (they start with 0 ERC20 balance) + await ensureMintedAndAllowedTokens( + [buyerFundedWallet, sellerFundedWallet], + undefined, + false + ); + // Buyer creates a buyer-initiated offer with ERC20 exchange token. // sellerDeposit is 0 to simplify the test setup. const buyerInitiatedOffer = await createOffer(buyerCoreSDKBuyer, {