@hypercerts-org/marketplace-sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { BigNumberish, ContractTransactionResponse, Overrides, Provider, Signer, TypedDataDomain } from "ethers";
2
+ import SafeApiKit from "@safe-global/api-kit";
2
3
  import { Addresses, ChainId, ContractMethods, CreateDirectFractionsSaleMakerAskInput, CreateFractionalSaleMakerAskInput, CreateMakerAskOutput, CreateMakerBidOutput, CreateMakerInput, Currencies, Maker, MerkleTree, Order, OrderValidatorCode, SignMerkleTreeOrdersOutput, StrategyInfo, StrategyType, Taker } from "./types";
3
4
  import { ApiClient } from "./utils/api";
4
5
  import { WalletClient } from "viem";
@@ -63,7 +64,9 @@ export declare class HypercertExchangeClient {
63
64
  * @param CreateMakerInput
64
65
  * @returns the maker object, isTransferManagerApproved, and isTransferManagerApproved
65
66
  */
66
- createMakerAsk({ collection, strategyId, collectionType, subsetNonce, orderNonce, endTime, price, itemIds, currency, startTime, additionalParameters, }: CreateMakerInput): Promise<CreateMakerAskOutput>;
67
+ createMakerAsk({ collection, strategyId, collectionType, subsetNonce, orderNonce, endTime, price, itemIds, currency, startTime, additionalParameters, safeAddress, }: CreateMakerInput & {
68
+ safeAddress?: string;
69
+ }): Promise<CreateMakerAskOutput>;
67
70
  /**
68
71
  * Create a maker bid object ready to be signed
69
72
  * @param CreateMakerInput
@@ -84,6 +87,14 @@ export declare class HypercertExchangeClient {
84
87
  * @returns Signature
85
88
  */
86
89
  signMakerOrder(maker: Maker): Promise<string>;
90
+ /**
91
+ * Create a maker order message and upload it to the Safe Transaction Service to be signed in the Safe app
92
+ * @param maker Order to be signed by the user
93
+ * @param safeAddress Address of the Safe to use
94
+ * @param safeApiKit Optional pre-initialized Safe API Kit instance
95
+ * @returns Signature
96
+ */
97
+ signMakerOrderSafe(maker: Maker, safeAddress: string, safeApiKit?: SafeApiKit): Promise<string>;
87
98
  /**
88
99
  * Sign multiple maker orders with a single signature
89
100
  * /!\ Use this function for UI implementation only
@@ -147,6 +158,14 @@ export declare class HypercertExchangeClient {
147
158
  * @returns ContractTransaction
148
159
  */
149
160
  approveAllCollectionItems(collectionAddress: string, approved?: boolean, overrides?: Overrides): Promise<ContractTransactionResponse>;
161
+ /**
162
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
163
+ * @param collectionAddress Address of the collection to be approved
164
+ * @param approved true to approve, false to revoke the approval (default to true)
165
+ * @param safeAddress Address of the Safe to use
166
+ * @returns Safe transaction hash
167
+ */
168
+ approveAllCollectionItemsSafe(safeAddress: string, collectionAddress: string, approved?: boolean): Promise<string>;
150
169
  /**
151
170
  * Approve an ERC20 to be used as a currency on the Hypercert Exchange.
152
171
  * The spender is the HypercertExchangeProtocol contract.
@@ -210,7 +229,7 @@ export declare class HypercertExchangeClient {
210
229
  * @param orders List of orders to be checked
211
230
  * @param overrides Call overrides (optional)
212
231
  */
213
- checkOrdersValidity(orders: Order[], overrides?: Overrides): Promise<{
232
+ checkOrdersValidity(orders: Omit<Order, "createdAt" | "invalidated" | "validator_codes">[], overrides?: Overrides): Promise<{
214
233
  id: string;
215
234
  valid: boolean;
216
235
  validatorCodes: OrderValidatorCode[];
@@ -227,11 +246,31 @@ export declare class HypercertExchangeClient {
227
246
  * @param CreateDirectFractionsSaleMakerAskInput
228
247
  */
229
248
  createDirectFractionsSaleMakerAsk({ itemIds, price, startTime, endTime, currency, additionalParameters, }: CreateDirectFractionsSaleMakerAskInput): Promise<CreateMakerAskOutput>;
249
+ /**
250
+ * Create a maker ask for a collection or singular offer of fractions using Safe
251
+ * @param CreateDirectFractionsSaleMakerAskInput
252
+ */
253
+ createDirectFractionsSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, additionalParameters, safeAddress, }: CreateDirectFractionsSaleMakerAskInput & {
254
+ safeAddress: string;
255
+ }): Promise<CreateMakerAskOutput>;
230
256
  /**
231
257
  * Create a maker ask to let the buyer decide how much of a fraction they want to buy
232
258
  * @param CreateFractionalSaleMakerInput
233
259
  */
234
260
  createFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, }: CreateFractionalSaleMakerAskInput): Promise<CreateMakerAskOutput>;
261
+ /**
262
+ * Create a maker ask to let the buyer decide how much of a fraction they want to buy using Safe
263
+ * @param CreateFractionalSaleMakerInput
264
+ */
265
+ createFractionalSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }: CreateFractionalSaleMakerAskInput & {
266
+ safeAddress: string;
267
+ }): Promise<CreateMakerAskOutput>;
268
+ /**
269
+ * Prepare a fractional sale maker ask with common logic for both regular and Safe transactions
270
+ * @param params CreateFractionalSaleMakerInput with optional safeAddress
271
+ * @private
272
+ */
273
+ private prepareFractionalSaleMakerAsk;
235
274
  /**
236
275
  * Create a taker bid for buying part of a fraction
237
276
  * @param maker Maker order
@@ -251,14 +290,26 @@ export declare class HypercertExchangeClient {
251
290
  }): Promise<{
252
291
  success: boolean;
253
292
  }>;
293
+ /**
294
+ * Register the order with the hypercerts marketplace API for Safe transactions
295
+ * @param messageHash The message hash from the Safe transaction
296
+ */
297
+ registerOrderSafe({ messageHash }: {
298
+ messageHash: string;
299
+ }): Promise<{
300
+ success: boolean;
301
+ }>;
254
302
  /**
255
303
  * Delete the order from the hypercerts marketplace API
256
304
  * @param orderId Order ID
257
305
  */
258
306
  deleteOrder(orderId: string): Promise<boolean>;
259
307
  /**
260
- * Bundle approval operations into a single Safe transaction
308
+ * Bundle the following operations into a single Safe transaction:
309
+ * - grantApprovals on the TransferManager
310
+ * - setApprovalForAll on the collection
261
311
  * @param safeAddress The address of the Safe contract
312
+ * @param walletClient Connected wallet client
262
313
  * @param collectionAddress Address of the collection to approve
263
314
  * @returns Transaction hash
264
315
  */
package/dist/index.cjs.js CHANGED
@@ -7640,6 +7640,7 @@ class ApiClient {
7640
7640
  },
7641
7641
  body: JSON.stringify({
7642
7642
  ...orderWithoutGlobalNonce,
7643
+ type: "eoa",
7643
7644
  globalNonce: globalNonce.toString(10),
7644
7645
  price: order.price.toString(10),
7645
7646
  quoteType,
@@ -7649,6 +7650,24 @@ class ApiClient {
7649
7650
  }),
7650
7651
  }).then((res) => this.handleResponse(res));
7651
7652
  };
7653
+ /**
7654
+ * Registers order in the marketplace API for Safe transactions
7655
+ * @param messageHash The message hash from the Safe transaction
7656
+ * @param chainId Chain ID
7657
+ */
7658
+ this.registerOrderSafe = async ({ messageHash, chainId, }) => {
7659
+ return fetch(`${this._baseUrl}/marketplace/orders/`, {
7660
+ method: "POST",
7661
+ headers: {
7662
+ "Content-Type": "application/json",
7663
+ },
7664
+ body: JSON.stringify({
7665
+ type: "multisig",
7666
+ messageHash,
7667
+ chainId,
7668
+ }),
7669
+ }).then((res) => this.handleResponse(res));
7670
+ };
7652
7671
  /**
7653
7672
  * @deprecated use GraphQL API instead
7654
7673
  * Fetch existing open orders from the marketplace API
@@ -7755,6 +7774,7 @@ class SafeTransactionBuilder {
7755
7774
  const nonce = await this.apiKit.getNextNonce(safeAddress);
7756
7775
  const safeTx = await connected.createTransaction({
7757
7776
  transactions,
7777
+ onlyCalls: true,
7758
7778
  options: {
7759
7779
  nonce,
7760
7780
  },
@@ -7826,8 +7846,108 @@ class SafeTransactionBuilder {
7826
7846
  }));
7827
7847
  return this.performSafeTransactions(safeAddress, transactions);
7828
7848
  }
7849
+ /**
7850
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
7851
+ * @internal
7852
+ */
7853
+ approveAllCollectionItems(safeAddress, collectionAddress, approved = true) {
7854
+ const erc721Contract = new ethers.Contract(collectionAddress, abiIERC721);
7855
+ const transactions = [
7856
+ {
7857
+ to: collectionAddress,
7858
+ data: erc721Contract.interface.encodeFunctionData("setApprovalForAll", [
7859
+ this.addresses.TRANSFER_MANAGER_V2,
7860
+ approved,
7861
+ ]),
7862
+ value: "0",
7863
+ },
7864
+ ];
7865
+ return this.performSafeTransactions(safeAddress, transactions);
7866
+ }
7829
7867
  }
7830
7868
 
7869
+ class SafeMessages {
7870
+ /**
7871
+ * @param address - The address of the Safe
7872
+ * @param chainId - The chainId where the Safe is deployed
7873
+ * @param provider - An EIP1193 compatible provider
7874
+ * @param safeApiKit - A pre-initialized Safe API Kit e.g. when on a network with a 3rd party transaction service deployment
7875
+ */
7876
+ constructor(address, chainId, provider, safeApiKit) {
7877
+ this.address = address;
7878
+ this.chainId = chainId;
7879
+ this.provider = provider;
7880
+ this.apiKit = safeApiKit || new SafeApiKit({ chainId: BigInt(chainId) });
7881
+ }
7882
+ async signAndSubmit(values, types, primaryType) {
7883
+ try {
7884
+ const { messageHash } = await this.initiateSigning({
7885
+ types: {
7886
+ ...types,
7887
+ },
7888
+ primaryType,
7889
+ message: values,
7890
+ });
7891
+ return messageHash;
7892
+ }
7893
+ catch (error) {
7894
+ console.error("[signAndSubmit] error", error instanceof Error ? error.message : error);
7895
+ throw error instanceof Error ? error : new Error("Error signing and submitting message");
7896
+ }
7897
+ }
7898
+ async initiateSigning(config) {
7899
+ if (!this.chainId)
7900
+ throw new Error("No chainId found");
7901
+ const safe = await Safe.init({
7902
+ provider: this.provider,
7903
+ safeAddress: this.address,
7904
+ });
7905
+ const domain = {
7906
+ name: DOMAIN_NAME,
7907
+ version: DOMAIN_VERSION.toString(),
7908
+ chainId: parseInt(this.chainId.toString()),
7909
+ verifyingContract: this.address,
7910
+ };
7911
+ const typedData = {
7912
+ domain,
7913
+ types: {
7914
+ ...DOMAIN_TYPE,
7915
+ ...config.types,
7916
+ },
7917
+ primaryType: config.primaryType,
7918
+ message: config.message,
7919
+ };
7920
+ const safeMessage = await safe.createMessage(typedData);
7921
+ const signature = await safe.signTypedData(safeMessage);
7922
+ try {
7923
+ await this.apiKit.addMessage(this.address, {
7924
+ message: typedData,
7925
+ signature: Safe.buildSignatureBytes([signature]),
7926
+ });
7927
+ }
7928
+ catch (error) {
7929
+ console.error("Error adding message to Safe API:", error);
7930
+ throw error;
7931
+ }
7932
+ const rawMessageHash = this.calculateMessageHash(domain, config);
7933
+ const safeHash = await safe.getSafeMessageHash(rawMessageHash);
7934
+ return {
7935
+ messageHash: safeHash,
7936
+ };
7937
+ }
7938
+ calculateMessageHash(domain, config) {
7939
+ return ethers.TypedDataEncoder.hash(domain, config.types, config.message);
7940
+ }
7941
+ }
7942
+ const DOMAIN_TYPE = {
7943
+ EIP712Domain: [
7944
+ { name: "name", type: "string" },
7945
+ { name: "version", type: "string" },
7946
+ { name: "chainId", type: "uint256" },
7947
+ { name: "verifyingContract", type: "address" },
7948
+ ],
7949
+ };
7950
+
7831
7951
  /**
7832
7952
  * HypercertExchange
7833
7953
  * This class provides helpers to interact with the HypercertExchange V2 contracts
@@ -7890,12 +8010,12 @@ class HypercertExchangeClient {
7890
8010
  * @param CreateMakerInput
7891
8011
  * @returns the maker object, isTransferManagerApproved, and isTransferManagerApproved
7892
8012
  */
7893
- async createMakerAsk({ collection, strategyId, collectionType, subsetNonce, orderNonce, endTime, price, itemIds, currency = ethers.ZeroAddress, startTime = Math.floor(Date.now() / 1000), additionalParameters = [], }) {
7894
- const signer = this.getSigner();
8013
+ async createMakerAsk({ collection, strategyId, collectionType, subsetNonce, orderNonce, endTime, price, itemIds, currency = ethers.ZeroAddress, startTime = Math.floor(Date.now() / 1000), additionalParameters = [], safeAddress, }) {
7895
8014
  if (!this.isTimestampValid(startTime) || !this.isTimestampValid(endTime)) {
7896
8015
  throw new ErrorTimestamp();
7897
8016
  }
7898
- const signerAddress = await signer.getAddress();
8017
+ // Use safeAddress if provided, otherwise get from signer
8018
+ const signerAddress = safeAddress || await this.getSigner().getAddress();
7899
8019
  const spenderAddress = this.addresses.TRANSFER_MANAGER_V2;
7900
8020
  // Use this.provider (MulticallProvider) in order to batch the calls
7901
8021
  const [isCollectionApproved, userBidAskNonce, isTransferManagerApproved] = await Promise.all([
@@ -7992,6 +8112,26 @@ class HypercertExchangeClient {
7992
8112
  const signer = this.getSigner();
7993
8113
  return await signMakerOrder(signer, this.getTypedDataDomain(), maker);
7994
8114
  }
8115
+ /**
8116
+ * Create a maker order message and upload it to the Safe Transaction Service to be signed in the Safe app
8117
+ * @param maker Order to be signed by the user
8118
+ * @param safeAddress Address of the Safe to use
8119
+ * @param safeApiKit Optional pre-initialized Safe API Kit instance
8120
+ * @returns Signature
8121
+ */
8122
+ async signMakerOrderSafe(maker, safeAddress, safeApiKit) {
8123
+ if (!this.walletClient) {
8124
+ throw new Error("wallet client is required to sign a maker order using Safe");
8125
+ }
8126
+ const safe = new SafeMessages(safeAddress, this.chainId,
8127
+ // The underlying provider is an Eip1193Provider, but the type is not exported
8128
+ this.walletClient, safeApiKit);
8129
+ // The assertion to unknown and Record<string, unknown> is necessary because Maker is a closed type while
8130
+ // Record<string, unknown> is not. Thus TypeScript will complain about the types having no overlap. But we know
8131
+ // that Maker is a Record<string, unknown> and sign() is not trying to get keys out of the Record that don't
8132
+ // exist on Maker, so we can safely type assert here.
8133
+ return safe.signAndSubmit(maker, makerTypes, "Maker");
8134
+ }
7995
8135
  /**
7996
8136
  * Sign multiple maker orders with a single signature
7997
8137
  * /!\ Use this function for UI implementation only
@@ -8088,6 +8228,20 @@ class HypercertExchangeClient {
8088
8228
  const spenderAddress = this.addresses.TRANSFER_MANAGER_V2;
8089
8229
  return setApprovalForAll(signer, collectionAddress, spenderAddress, approved, overrides);
8090
8230
  }
8231
+ /**
8232
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
8233
+ * @param collectionAddress Address of the collection to be approved
8234
+ * @param approved true to approve, false to revoke the approval (default to true)
8235
+ * @param safeAddress Address of the Safe to use
8236
+ * @returns Safe transaction hash
8237
+ */
8238
+ approveAllCollectionItemsSafe(safeAddress, collectionAddress, approved = true) {
8239
+ if (!this.walletClient) {
8240
+ throw new Error("No wallet client");
8241
+ }
8242
+ const safeTransactionBuilder = new SafeTransactionBuilder(this.walletClient, this.chainId, this.addresses);
8243
+ return safeTransactionBuilder.approveAllCollectionItems(safeAddress, collectionAddress, approved);
8244
+ }
8091
8245
  /**
8092
8246
  * Approve an ERC20 to be used as a currency on the Hypercert Exchange.
8093
8247
  * The spender is the HypercertExchangeProtocol contract.
@@ -8239,10 +8393,9 @@ class HypercertExchangeClient {
8239
8393
  if (!currency) {
8240
8394
  throw new ErrorCurrency();
8241
8395
  }
8242
- const chainId = this.chainId;
8243
8396
  const { nonce_counter } = await this.api.fetchOrderNonce({
8244
8397
  address,
8245
- chainId,
8398
+ chainId: this.chainId,
8246
8399
  });
8247
8400
  return this.createMakerAsk({
8248
8401
  // Defaults
@@ -8260,14 +8413,90 @@ class HypercertExchangeClient {
8260
8413
  additionalParameters,
8261
8414
  });
8262
8415
  }
8416
+ /**
8417
+ * Create a maker ask for a collection or singular offer of fractions using Safe
8418
+ * @param CreateDirectFractionsSaleMakerAskInput
8419
+ */
8420
+ async createDirectFractionsSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, additionalParameters = [], safeAddress, }) {
8421
+ if (!safeAddress) {
8422
+ throw new Error("Safe address is required for Safe transactions");
8423
+ }
8424
+ if (!currency) {
8425
+ throw new ErrorCurrency();
8426
+ }
8427
+ const { nonce_counter } = await this.api.fetchOrderNonce({
8428
+ address: safeAddress,
8429
+ chainId: this.chainId,
8430
+ });
8431
+ return this.createMakerAsk({
8432
+ // Defaults
8433
+ strategyId: exports.StrategyType.standard,
8434
+ collectionType: exports.CollectionType.HYPERCERT,
8435
+ collection: this.addresses.MINTER,
8436
+ subsetNonce: 0,
8437
+ currency,
8438
+ orderNonce: nonce_counter.toString(),
8439
+ // User specified
8440
+ itemIds,
8441
+ price,
8442
+ startTime,
8443
+ endTime,
8444
+ additionalParameters,
8445
+ safeAddress,
8446
+ });
8447
+ }
8263
8448
  /**
8264
8449
  * Create a maker ask to let the buyer decide how much of a fraction they want to buy
8265
8450
  * @param CreateFractionalSaleMakerInput
8266
8451
  */
8267
8452
  async createFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, }) {
8268
- const address = await this.signer?.getAddress();
8453
+ return this.prepareFractionalSaleMakerAsk({
8454
+ itemIds,
8455
+ price,
8456
+ startTime,
8457
+ endTime,
8458
+ currency,
8459
+ maxUnitAmount,
8460
+ minUnitAmount,
8461
+ minUnitsToKeep,
8462
+ sellLeftoverFraction,
8463
+ root,
8464
+ });
8465
+ }
8466
+ /**
8467
+ * Create a maker ask to let the buyer decide how much of a fraction they want to buy using Safe
8468
+ * @param CreateFractionalSaleMakerInput
8469
+ */
8470
+ async createFractionalSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }) {
8471
+ if (!safeAddress) {
8472
+ throw new Error("Safe address is required for Safe transactions");
8473
+ }
8474
+ return this.prepareFractionalSaleMakerAsk({
8475
+ itemIds,
8476
+ price,
8477
+ startTime,
8478
+ endTime,
8479
+ currency,
8480
+ maxUnitAmount,
8481
+ minUnitAmount,
8482
+ minUnitsToKeep,
8483
+ sellLeftoverFraction,
8484
+ root,
8485
+ safeAddress,
8486
+ });
8487
+ }
8488
+ /**
8489
+ * Prepare a fractional sale maker ask with common logic for both regular and Safe transactions
8490
+ * @param params CreateFractionalSaleMakerInput with optional safeAddress
8491
+ * @private
8492
+ */
8493
+ async prepareFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }) {
8494
+ let address = safeAddress;
8269
8495
  if (!address) {
8270
- throw new Error("No signer address could be determined");
8496
+ address = await this.getSigner().getAddress();
8497
+ if (!address) {
8498
+ throw new Error("No signer address could be determined");
8499
+ }
8271
8500
  }
8272
8501
  if (!currency) {
8273
8502
  throw new ErrorCurrency();
@@ -8291,6 +8520,7 @@ class HypercertExchangeClient {
8291
8520
  price,
8292
8521
  startTime,
8293
8522
  endTime,
8523
+ safeAddress,
8294
8524
  };
8295
8525
  if (root) {
8296
8526
  return this.createMakerAsk({
@@ -8337,6 +8567,16 @@ class HypercertExchangeClient {
8337
8567
  chainId,
8338
8568
  });
8339
8569
  }
8570
+ /**
8571
+ * Register the order with the hypercerts marketplace API for Safe transactions
8572
+ * @param messageHash The message hash from the Safe transaction
8573
+ */
8574
+ async registerOrderSafe({ messageHash }) {
8575
+ return this.api.registerOrderSafe({
8576
+ messageHash,
8577
+ chainId: this.chainId,
8578
+ });
8579
+ }
8340
8580
  /**
8341
8581
  * Delete the order from the hypercerts marketplace API
8342
8582
  * @param orderId Order ID
@@ -8350,8 +8590,11 @@ class HypercertExchangeClient {
8350
8590
  return this.api.deleteOrder(orderId, signedMessage);
8351
8591
  }
8352
8592
  /**
8353
- * Bundle approval operations into a single Safe transaction
8593
+ * Bundle the following operations into a single Safe transaction:
8594
+ * - grantApprovals on the TransferManager
8595
+ * - setApprovalForAll on the collection
8354
8596
  * @param safeAddress The address of the Safe contract
8597
+ * @param walletClient Connected wallet client
8355
8598
  * @param collectionAddress Address of the collection to approve
8356
8599
  * @returns Transaction hash
8357
8600
  */
package/dist/index.esm.js CHANGED
@@ -2,7 +2,7 @@ import { Contract, ZeroAddress, AbiCoder, TypedDataEncoder, keccak256, solidityP
2
2
  import { HypercertExchangeAbi, TransferManagerAbi, OrderValidatorV2AAbi, deployments, asDeployedChain as asDeployedChain$2 } from '@hypercerts-org/contracts';
3
3
  import { MerkleTree } from 'merkletreejs';
4
4
  import { CONSTANTS } from '@hypercerts-org/sdk';
5
- import Safe from '@safe-global/protocol-kit';
5
+ import Safe, { buildSignatureBytes } from '@safe-global/protocol-kit';
6
6
  import SafeApiKit from '@safe-global/api-kit';
7
7
 
8
8
  var abiIERC721 = [
@@ -7638,6 +7638,7 @@ class ApiClient {
7638
7638
  },
7639
7639
  body: JSON.stringify({
7640
7640
  ...orderWithoutGlobalNonce,
7641
+ type: "eoa",
7641
7642
  globalNonce: globalNonce.toString(10),
7642
7643
  price: order.price.toString(10),
7643
7644
  quoteType,
@@ -7647,6 +7648,24 @@ class ApiClient {
7647
7648
  }),
7648
7649
  }).then((res) => this.handleResponse(res));
7649
7650
  };
7651
+ /**
7652
+ * Registers order in the marketplace API for Safe transactions
7653
+ * @param messageHash The message hash from the Safe transaction
7654
+ * @param chainId Chain ID
7655
+ */
7656
+ this.registerOrderSafe = async ({ messageHash, chainId, }) => {
7657
+ return fetch(`${this._baseUrl}/marketplace/orders/`, {
7658
+ method: "POST",
7659
+ headers: {
7660
+ "Content-Type": "application/json",
7661
+ },
7662
+ body: JSON.stringify({
7663
+ type: "multisig",
7664
+ messageHash,
7665
+ chainId,
7666
+ }),
7667
+ }).then((res) => this.handleResponse(res));
7668
+ };
7650
7669
  /**
7651
7670
  * @deprecated use GraphQL API instead
7652
7671
  * Fetch existing open orders from the marketplace API
@@ -7753,6 +7772,7 @@ class SafeTransactionBuilder {
7753
7772
  const nonce = await this.apiKit.getNextNonce(safeAddress);
7754
7773
  const safeTx = await connected.createTransaction({
7755
7774
  transactions,
7775
+ onlyCalls: true,
7756
7776
  options: {
7757
7777
  nonce,
7758
7778
  },
@@ -7824,8 +7844,108 @@ class SafeTransactionBuilder {
7824
7844
  }));
7825
7845
  return this.performSafeTransactions(safeAddress, transactions);
7826
7846
  }
7847
+ /**
7848
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
7849
+ * @internal
7850
+ */
7851
+ approveAllCollectionItems(safeAddress, collectionAddress, approved = true) {
7852
+ const erc721Contract = new Contract(collectionAddress, abiIERC721);
7853
+ const transactions = [
7854
+ {
7855
+ to: collectionAddress,
7856
+ data: erc721Contract.interface.encodeFunctionData("setApprovalForAll", [
7857
+ this.addresses.TRANSFER_MANAGER_V2,
7858
+ approved,
7859
+ ]),
7860
+ value: "0",
7861
+ },
7862
+ ];
7863
+ return this.performSafeTransactions(safeAddress, transactions);
7864
+ }
7827
7865
  }
7828
7866
 
7867
+ class SafeMessages {
7868
+ /**
7869
+ * @param address - The address of the Safe
7870
+ * @param chainId - The chainId where the Safe is deployed
7871
+ * @param provider - An EIP1193 compatible provider
7872
+ * @param safeApiKit - A pre-initialized Safe API Kit e.g. when on a network with a 3rd party transaction service deployment
7873
+ */
7874
+ constructor(address, chainId, provider, safeApiKit) {
7875
+ this.address = address;
7876
+ this.chainId = chainId;
7877
+ this.provider = provider;
7878
+ this.apiKit = safeApiKit || new SafeApiKit({ chainId: BigInt(chainId) });
7879
+ }
7880
+ async signAndSubmit(values, types, primaryType) {
7881
+ try {
7882
+ const { messageHash } = await this.initiateSigning({
7883
+ types: {
7884
+ ...types,
7885
+ },
7886
+ primaryType,
7887
+ message: values,
7888
+ });
7889
+ return messageHash;
7890
+ }
7891
+ catch (error) {
7892
+ console.error("[signAndSubmit] error", error instanceof Error ? error.message : error);
7893
+ throw error instanceof Error ? error : new Error("Error signing and submitting message");
7894
+ }
7895
+ }
7896
+ async initiateSigning(config) {
7897
+ if (!this.chainId)
7898
+ throw new Error("No chainId found");
7899
+ const safe = await Safe.init({
7900
+ provider: this.provider,
7901
+ safeAddress: this.address,
7902
+ });
7903
+ const domain = {
7904
+ name: DOMAIN_NAME,
7905
+ version: DOMAIN_VERSION.toString(),
7906
+ chainId: parseInt(this.chainId.toString()),
7907
+ verifyingContract: this.address,
7908
+ };
7909
+ const typedData = {
7910
+ domain,
7911
+ types: {
7912
+ ...DOMAIN_TYPE,
7913
+ ...config.types,
7914
+ },
7915
+ primaryType: config.primaryType,
7916
+ message: config.message,
7917
+ };
7918
+ const safeMessage = await safe.createMessage(typedData);
7919
+ const signature = await safe.signTypedData(safeMessage);
7920
+ try {
7921
+ await this.apiKit.addMessage(this.address, {
7922
+ message: typedData,
7923
+ signature: buildSignatureBytes([signature]),
7924
+ });
7925
+ }
7926
+ catch (error) {
7927
+ console.error("Error adding message to Safe API:", error);
7928
+ throw error;
7929
+ }
7930
+ const rawMessageHash = this.calculateMessageHash(domain, config);
7931
+ const safeHash = await safe.getSafeMessageHash(rawMessageHash);
7932
+ return {
7933
+ messageHash: safeHash,
7934
+ };
7935
+ }
7936
+ calculateMessageHash(domain, config) {
7937
+ return TypedDataEncoder.hash(domain, config.types, config.message);
7938
+ }
7939
+ }
7940
+ const DOMAIN_TYPE = {
7941
+ EIP712Domain: [
7942
+ { name: "name", type: "string" },
7943
+ { name: "version", type: "string" },
7944
+ { name: "chainId", type: "uint256" },
7945
+ { name: "verifyingContract", type: "address" },
7946
+ ],
7947
+ };
7948
+
7829
7949
  /**
7830
7950
  * HypercertExchange
7831
7951
  * This class provides helpers to interact with the HypercertExchange V2 contracts
@@ -7888,12 +8008,12 @@ class HypercertExchangeClient {
7888
8008
  * @param CreateMakerInput
7889
8009
  * @returns the maker object, isTransferManagerApproved, and isTransferManagerApproved
7890
8010
  */
7891
- async createMakerAsk({ collection, strategyId, collectionType, subsetNonce, orderNonce, endTime, price, itemIds, currency = ZeroAddress, startTime = Math.floor(Date.now() / 1000), additionalParameters = [], }) {
7892
- const signer = this.getSigner();
8011
+ async createMakerAsk({ collection, strategyId, collectionType, subsetNonce, orderNonce, endTime, price, itemIds, currency = ZeroAddress, startTime = Math.floor(Date.now() / 1000), additionalParameters = [], safeAddress, }) {
7893
8012
  if (!this.isTimestampValid(startTime) || !this.isTimestampValid(endTime)) {
7894
8013
  throw new ErrorTimestamp();
7895
8014
  }
7896
- const signerAddress = await signer.getAddress();
8015
+ // Use safeAddress if provided, otherwise get from signer
8016
+ const signerAddress = safeAddress || await this.getSigner().getAddress();
7897
8017
  const spenderAddress = this.addresses.TRANSFER_MANAGER_V2;
7898
8018
  // Use this.provider (MulticallProvider) in order to batch the calls
7899
8019
  const [isCollectionApproved, userBidAskNonce, isTransferManagerApproved] = await Promise.all([
@@ -7990,6 +8110,26 @@ class HypercertExchangeClient {
7990
8110
  const signer = this.getSigner();
7991
8111
  return await signMakerOrder(signer, this.getTypedDataDomain(), maker);
7992
8112
  }
8113
+ /**
8114
+ * Create a maker order message and upload it to the Safe Transaction Service to be signed in the Safe app
8115
+ * @param maker Order to be signed by the user
8116
+ * @param safeAddress Address of the Safe to use
8117
+ * @param safeApiKit Optional pre-initialized Safe API Kit instance
8118
+ * @returns Signature
8119
+ */
8120
+ async signMakerOrderSafe(maker, safeAddress, safeApiKit) {
8121
+ if (!this.walletClient) {
8122
+ throw new Error("wallet client is required to sign a maker order using Safe");
8123
+ }
8124
+ const safe = new SafeMessages(safeAddress, this.chainId,
8125
+ // The underlying provider is an Eip1193Provider, but the type is not exported
8126
+ this.walletClient, safeApiKit);
8127
+ // The assertion to unknown and Record<string, unknown> is necessary because Maker is a closed type while
8128
+ // Record<string, unknown> is not. Thus TypeScript will complain about the types having no overlap. But we know
8129
+ // that Maker is a Record<string, unknown> and sign() is not trying to get keys out of the Record that don't
8130
+ // exist on Maker, so we can safely type assert here.
8131
+ return safe.signAndSubmit(maker, makerTypes, "Maker");
8132
+ }
7993
8133
  /**
7994
8134
  * Sign multiple maker orders with a single signature
7995
8135
  * /!\ Use this function for UI implementation only
@@ -8086,6 +8226,20 @@ class HypercertExchangeClient {
8086
8226
  const spenderAddress = this.addresses.TRANSFER_MANAGER_V2;
8087
8227
  return setApprovalForAll(signer, collectionAddress, spenderAddress, approved, overrides);
8088
8228
  }
8229
+ /**
8230
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
8231
+ * @param collectionAddress Address of the collection to be approved
8232
+ * @param approved true to approve, false to revoke the approval (default to true)
8233
+ * @param safeAddress Address of the Safe to use
8234
+ * @returns Safe transaction hash
8235
+ */
8236
+ approveAllCollectionItemsSafe(safeAddress, collectionAddress, approved = true) {
8237
+ if (!this.walletClient) {
8238
+ throw new Error("No wallet client");
8239
+ }
8240
+ const safeTransactionBuilder = new SafeTransactionBuilder(this.walletClient, this.chainId, this.addresses);
8241
+ return safeTransactionBuilder.approveAllCollectionItems(safeAddress, collectionAddress, approved);
8242
+ }
8089
8243
  /**
8090
8244
  * Approve an ERC20 to be used as a currency on the Hypercert Exchange.
8091
8245
  * The spender is the HypercertExchangeProtocol contract.
@@ -8237,10 +8391,9 @@ class HypercertExchangeClient {
8237
8391
  if (!currency) {
8238
8392
  throw new ErrorCurrency();
8239
8393
  }
8240
- const chainId = this.chainId;
8241
8394
  const { nonce_counter } = await this.api.fetchOrderNonce({
8242
8395
  address,
8243
- chainId,
8396
+ chainId: this.chainId,
8244
8397
  });
8245
8398
  return this.createMakerAsk({
8246
8399
  // Defaults
@@ -8258,14 +8411,90 @@ class HypercertExchangeClient {
8258
8411
  additionalParameters,
8259
8412
  });
8260
8413
  }
8414
+ /**
8415
+ * Create a maker ask for a collection or singular offer of fractions using Safe
8416
+ * @param CreateDirectFractionsSaleMakerAskInput
8417
+ */
8418
+ async createDirectFractionsSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, additionalParameters = [], safeAddress, }) {
8419
+ if (!safeAddress) {
8420
+ throw new Error("Safe address is required for Safe transactions");
8421
+ }
8422
+ if (!currency) {
8423
+ throw new ErrorCurrency();
8424
+ }
8425
+ const { nonce_counter } = await this.api.fetchOrderNonce({
8426
+ address: safeAddress,
8427
+ chainId: this.chainId,
8428
+ });
8429
+ return this.createMakerAsk({
8430
+ // Defaults
8431
+ strategyId: StrategyType.standard,
8432
+ collectionType: CollectionType.HYPERCERT,
8433
+ collection: this.addresses.MINTER,
8434
+ subsetNonce: 0,
8435
+ currency,
8436
+ orderNonce: nonce_counter.toString(),
8437
+ // User specified
8438
+ itemIds,
8439
+ price,
8440
+ startTime,
8441
+ endTime,
8442
+ additionalParameters,
8443
+ safeAddress,
8444
+ });
8445
+ }
8261
8446
  /**
8262
8447
  * Create a maker ask to let the buyer decide how much of a fraction they want to buy
8263
8448
  * @param CreateFractionalSaleMakerInput
8264
8449
  */
8265
8450
  async createFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, }) {
8266
- const address = await this.signer?.getAddress();
8451
+ return this.prepareFractionalSaleMakerAsk({
8452
+ itemIds,
8453
+ price,
8454
+ startTime,
8455
+ endTime,
8456
+ currency,
8457
+ maxUnitAmount,
8458
+ minUnitAmount,
8459
+ minUnitsToKeep,
8460
+ sellLeftoverFraction,
8461
+ root,
8462
+ });
8463
+ }
8464
+ /**
8465
+ * Create a maker ask to let the buyer decide how much of a fraction they want to buy using Safe
8466
+ * @param CreateFractionalSaleMakerInput
8467
+ */
8468
+ async createFractionalSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }) {
8469
+ if (!safeAddress) {
8470
+ throw new Error("Safe address is required for Safe transactions");
8471
+ }
8472
+ return this.prepareFractionalSaleMakerAsk({
8473
+ itemIds,
8474
+ price,
8475
+ startTime,
8476
+ endTime,
8477
+ currency,
8478
+ maxUnitAmount,
8479
+ minUnitAmount,
8480
+ minUnitsToKeep,
8481
+ sellLeftoverFraction,
8482
+ root,
8483
+ safeAddress,
8484
+ });
8485
+ }
8486
+ /**
8487
+ * Prepare a fractional sale maker ask with common logic for both regular and Safe transactions
8488
+ * @param params CreateFractionalSaleMakerInput with optional safeAddress
8489
+ * @private
8490
+ */
8491
+ async prepareFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }) {
8492
+ let address = safeAddress;
8267
8493
  if (!address) {
8268
- throw new Error("No signer address could be determined");
8494
+ address = await this.getSigner().getAddress();
8495
+ if (!address) {
8496
+ throw new Error("No signer address could be determined");
8497
+ }
8269
8498
  }
8270
8499
  if (!currency) {
8271
8500
  throw new ErrorCurrency();
@@ -8289,6 +8518,7 @@ class HypercertExchangeClient {
8289
8518
  price,
8290
8519
  startTime,
8291
8520
  endTime,
8521
+ safeAddress,
8292
8522
  };
8293
8523
  if (root) {
8294
8524
  return this.createMakerAsk({
@@ -8335,6 +8565,16 @@ class HypercertExchangeClient {
8335
8565
  chainId,
8336
8566
  });
8337
8567
  }
8568
+ /**
8569
+ * Register the order with the hypercerts marketplace API for Safe transactions
8570
+ * @param messageHash The message hash from the Safe transaction
8571
+ */
8572
+ async registerOrderSafe({ messageHash }) {
8573
+ return this.api.registerOrderSafe({
8574
+ messageHash,
8575
+ chainId: this.chainId,
8576
+ });
8577
+ }
8338
8578
  /**
8339
8579
  * Delete the order from the hypercerts marketplace API
8340
8580
  * @param orderId Order ID
@@ -8348,8 +8588,11 @@ class HypercertExchangeClient {
8348
8588
  return this.api.deleteOrder(orderId, signedMessage);
8349
8589
  }
8350
8590
  /**
8351
- * Bundle approval operations into a single Safe transaction
8591
+ * Bundle the following operations into a single Safe transaction:
8592
+ * - grantApprovals on the TransferManager
8593
+ * - setApprovalForAll on the collection
8352
8594
  * @param safeAddress The address of the Safe contract
8595
+ * @param walletClient Connected wallet client
8353
8596
  * @param collectionAddress Address of the collection to approve
8354
8597
  * @returns Transaction hash
8355
8598
  */
@@ -0,0 +1,22 @@
1
+ import { Eip1193Provider } from "@safe-global/protocol-kit";
2
+ import SafeApiKit from "@safe-global/api-kit";
3
+ import { ChainId } from "../types";
4
+ export declare class SafeMessages {
5
+ private address;
6
+ private chainId;
7
+ private provider;
8
+ private apiKit;
9
+ /**
10
+ * @param address - The address of the Safe
11
+ * @param chainId - The chainId where the Safe is deployed
12
+ * @param provider - An EIP1193 compatible provider
13
+ * @param safeApiKit - A pre-initialized Safe API Kit e.g. when on a network with a 3rd party transaction service deployment
14
+ */
15
+ constructor(address: `0x${string}`, chainId: ChainId, provider: Eip1193Provider, safeApiKit?: SafeApiKit);
16
+ signAndSubmit(values: Record<string, unknown>, types: Record<string, Array<{
17
+ name: string;
18
+ type: string;
19
+ }>>, primaryType: string): Promise<string>;
20
+ private initiateSigning;
21
+ private calculateMessageHash;
22
+ }
@@ -27,6 +27,11 @@ export declare class SafeTransactionBuilder {
27
27
  * @internal
28
28
  */
29
29
  grantTransferManagerApproval(safeAddress: string, operators: string[]): Promise<string>;
30
+ /**
31
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
32
+ * @internal
33
+ */
34
+ approveAllCollectionItems(safeAddress: string, collectionAddress: string, approved?: boolean): Promise<string>;
30
35
  /**
31
36
  * Perform a series of Safe transactions in a single transaction
32
37
  * @internal
@@ -34,6 +34,17 @@ export declare class ApiClient {
34
34
  }) => Promise<{
35
35
  success: boolean;
36
36
  }>;
37
+ /**
38
+ * Registers order in the marketplace API for Safe transactions
39
+ * @param messageHash The message hash from the Safe transaction
40
+ * @param chainId Chain ID
41
+ */
42
+ registerOrderSafe: ({ messageHash, chainId, }: {
43
+ messageHash: string;
44
+ chainId: number;
45
+ }) => Promise<{
46
+ success: boolean;
47
+ }>;
37
48
  /**
38
49
  * @deprecated use GraphQL API instead
39
50
  * Fetch existing open orders from the marketplace API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypercerts-org/marketplace-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
@@ -43,6 +43,7 @@
43
43
  "peerDependencies": {
44
44
  "@safe-global/api-kit": "^2.5.7",
45
45
  "@safe-global/protocol-kit": "^5.2.0",
46
+ "@safe-global/types-kit": "^1.0.4",
46
47
  "ethers": "^6.6.2"
47
48
  },
48
49
  "devDependencies": {