@hypercerts-org/marketplace-sdk 0.6.0 → 0.8.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
@@ -672,7 +672,7 @@ var strategies = /*#__PURE__*/Object.freeze({
672
672
  });
673
673
 
674
674
  /** All possible supported currencies */
675
- const SUPPORTED_CURRENCIES = ["ETH", "WETH", "DAI", "CELO", "cUSD", "USDT", "USDC"];
675
+ const SUPPORTED_CURRENCIES = ["ETH", "WETH", "DAI", "CELO", "cUSD", "USDT", "USDC", "USDGLO"];
676
676
  /** List of supported chains */
677
677
  exports.ChainId = void 0;
678
678
  (function (ChainId) {
@@ -1150,6 +1150,7 @@ const currencyAddressesPerChain = {
1150
1150
  cUSD: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
1151
1151
  USDC: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
1152
1152
  USDT: "0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e",
1153
+ USDGLO: "0x4F604735c1cF31399C6E711D5962b2B3E0225AD3",
1153
1154
  },
1154
1155
  [exports.ChainId.ARBITRUM]: {
1155
1156
  ETH: ethers.ZeroAddress,
@@ -1205,6 +1206,11 @@ const getCurrencies = (chainId) => {
1205
1206
  address: currenciesForChain.USDT,
1206
1207
  decimals: 6,
1207
1208
  },
1209
+ USDGLO: {
1210
+ symbol: "USDGLO",
1211
+ address: currenciesForChain.USDGLO,
1212
+ decimals: 18,
1213
+ },
1208
1214
  };
1209
1215
  };
1210
1216
  const currenciesByNetwork = {
@@ -7640,6 +7646,7 @@ class ApiClient {
7640
7646
  },
7641
7647
  body: JSON.stringify({
7642
7648
  ...orderWithoutGlobalNonce,
7649
+ type: "eoa",
7643
7650
  globalNonce: globalNonce.toString(10),
7644
7651
  price: order.price.toString(10),
7645
7652
  quoteType,
@@ -7649,6 +7656,24 @@ class ApiClient {
7649
7656
  }),
7650
7657
  }).then((res) => this.handleResponse(res));
7651
7658
  };
7659
+ /**
7660
+ * Registers order in the marketplace API for Safe transactions
7661
+ * @param messageHash The message hash from the Safe transaction
7662
+ * @param chainId Chain ID
7663
+ */
7664
+ this.registerOrderSafe = async ({ messageHash, chainId, }) => {
7665
+ return fetch(`${this._baseUrl}/marketplace/orders/`, {
7666
+ method: "POST",
7667
+ headers: {
7668
+ "Content-Type": "application/json",
7669
+ },
7670
+ body: JSON.stringify({
7671
+ type: "multisig",
7672
+ messageHash,
7673
+ chainId,
7674
+ }),
7675
+ }).then((res) => this.handleResponse(res));
7676
+ };
7652
7677
  /**
7653
7678
  * @deprecated use GraphQL API instead
7654
7679
  * Fetch existing open orders from the marketplace API
@@ -7755,6 +7780,7 @@ class SafeTransactionBuilder {
7755
7780
  const nonce = await this.apiKit.getNextNonce(safeAddress);
7756
7781
  const safeTx = await connected.createTransaction({
7757
7782
  transactions,
7783
+ onlyCalls: true,
7758
7784
  options: {
7759
7785
  nonce,
7760
7786
  },
@@ -7826,7 +7852,107 @@ class SafeTransactionBuilder {
7826
7852
  }));
7827
7853
  return this.performSafeTransactions(safeAddress, transactions);
7828
7854
  }
7855
+ /**
7856
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
7857
+ * @internal
7858
+ */
7859
+ approveAllCollectionItems(safeAddress, collectionAddress, approved = true) {
7860
+ const erc721Contract = new ethers.Contract(collectionAddress, abiIERC721);
7861
+ const transactions = [
7862
+ {
7863
+ to: collectionAddress,
7864
+ data: erc721Contract.interface.encodeFunctionData("setApprovalForAll", [
7865
+ this.addresses.TRANSFER_MANAGER_V2,
7866
+ approved,
7867
+ ]),
7868
+ value: "0",
7869
+ },
7870
+ ];
7871
+ return this.performSafeTransactions(safeAddress, transactions);
7872
+ }
7873
+ }
7874
+
7875
+ class SafeMessages {
7876
+ /**
7877
+ * @param address - The address of the Safe
7878
+ * @param chainId - The chainId where the Safe is deployed
7879
+ * @param provider - An EIP1193 compatible provider
7880
+ * @param safeApiKit - A pre-initialized Safe API Kit e.g. when on a network with a 3rd party transaction service deployment
7881
+ */
7882
+ constructor(address, chainId, provider, safeApiKit) {
7883
+ this.address = address;
7884
+ this.chainId = chainId;
7885
+ this.provider = provider;
7886
+ this.apiKit = safeApiKit || new SafeApiKit({ chainId: BigInt(chainId) });
7887
+ }
7888
+ async signAndSubmit(values, types, primaryType) {
7889
+ try {
7890
+ const { messageHash } = await this.initiateSigning({
7891
+ types: {
7892
+ ...types,
7893
+ },
7894
+ primaryType,
7895
+ message: values,
7896
+ });
7897
+ return messageHash;
7898
+ }
7899
+ catch (error) {
7900
+ console.error("[signAndSubmit] error", error instanceof Error ? error.message : error);
7901
+ throw error instanceof Error ? error : new Error("Error signing and submitting message");
7902
+ }
7903
+ }
7904
+ async initiateSigning(config) {
7905
+ if (!this.chainId)
7906
+ throw new Error("No chainId found");
7907
+ const safe = await Safe.init({
7908
+ provider: this.provider,
7909
+ safeAddress: this.address,
7910
+ });
7911
+ const domain = {
7912
+ name: DOMAIN_NAME,
7913
+ version: DOMAIN_VERSION.toString(),
7914
+ chainId: parseInt(this.chainId.toString()),
7915
+ verifyingContract: this.address,
7916
+ };
7917
+ const typedData = {
7918
+ domain,
7919
+ types: {
7920
+ ...DOMAIN_TYPE,
7921
+ ...config.types,
7922
+ },
7923
+ primaryType: config.primaryType,
7924
+ message: config.message,
7925
+ };
7926
+ const safeMessage = await safe.createMessage(typedData);
7927
+ const signature = await safe.signTypedData(safeMessage);
7928
+ try {
7929
+ await this.apiKit.addMessage(this.address, {
7930
+ message: typedData,
7931
+ signature: Safe.buildSignatureBytes([signature]),
7932
+ });
7933
+ }
7934
+ catch (error) {
7935
+ console.error("Error adding message to Safe API:", error);
7936
+ throw error;
7937
+ }
7938
+ const rawMessageHash = this.calculateMessageHash(domain, config);
7939
+ const safeHash = await safe.getSafeMessageHash(rawMessageHash);
7940
+ return {
7941
+ messageHash: safeHash,
7942
+ };
7943
+ }
7944
+ calculateMessageHash(domain, config) {
7945
+ return ethers.TypedDataEncoder.hash(domain, config.types, config.message);
7946
+ }
7829
7947
  }
7948
+ const DOMAIN_TYPE = {
7949
+ EIP712Domain: [
7950
+ { name: "name", type: "string" },
7951
+ { name: "version", type: "string" },
7952
+ { name: "chainId", type: "uint256" },
7953
+ { name: "verifyingContract", type: "address" },
7954
+ ],
7955
+ };
7830
7956
 
7831
7957
  /**
7832
7958
  * HypercertExchange
@@ -7890,12 +8016,12 @@ class HypercertExchangeClient {
7890
8016
  * @param CreateMakerInput
7891
8017
  * @returns the maker object, isTransferManagerApproved, and isTransferManagerApproved
7892
8018
  */
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();
8019
+ async createMakerAsk({ collection, strategyId, collectionType, subsetNonce, orderNonce, endTime, price, itemIds, currency = ethers.ZeroAddress, startTime = Math.floor(Date.now() / 1000), additionalParameters = [], safeAddress, }) {
7895
8020
  if (!this.isTimestampValid(startTime) || !this.isTimestampValid(endTime)) {
7896
8021
  throw new ErrorTimestamp();
7897
8022
  }
7898
- const signerAddress = await signer.getAddress();
8023
+ // Use safeAddress if provided, otherwise get from signer
8024
+ const signerAddress = safeAddress || await this.getSigner().getAddress();
7899
8025
  const spenderAddress = this.addresses.TRANSFER_MANAGER_V2;
7900
8026
  // Use this.provider (MulticallProvider) in order to batch the calls
7901
8027
  const [isCollectionApproved, userBidAskNonce, isTransferManagerApproved] = await Promise.all([
@@ -7992,6 +8118,26 @@ class HypercertExchangeClient {
7992
8118
  const signer = this.getSigner();
7993
8119
  return await signMakerOrder(signer, this.getTypedDataDomain(), maker);
7994
8120
  }
8121
+ /**
8122
+ * Create a maker order message and upload it to the Safe Transaction Service to be signed in the Safe app
8123
+ * @param maker Order to be signed by the user
8124
+ * @param safeAddress Address of the Safe to use
8125
+ * @param safeApiKit Optional pre-initialized Safe API Kit instance
8126
+ * @returns Signature
8127
+ */
8128
+ async signMakerOrderSafe(maker, safeAddress, safeApiKit) {
8129
+ if (!this.walletClient) {
8130
+ throw new Error("wallet client is required to sign a maker order using Safe");
8131
+ }
8132
+ const safe = new SafeMessages(safeAddress, this.chainId,
8133
+ // The underlying provider is an Eip1193Provider, but the type is not exported
8134
+ this.walletClient, safeApiKit);
8135
+ // The assertion to unknown and Record<string, unknown> is necessary because Maker is a closed type while
8136
+ // Record<string, unknown> is not. Thus TypeScript will complain about the types having no overlap. But we know
8137
+ // that Maker is a Record<string, unknown> and sign() is not trying to get keys out of the Record that don't
8138
+ // exist on Maker, so we can safely type assert here.
8139
+ return safe.signAndSubmit(maker, makerTypes, "Maker");
8140
+ }
7995
8141
  /**
7996
8142
  * Sign multiple maker orders with a single signature
7997
8143
  * /!\ Use this function for UI implementation only
@@ -8088,6 +8234,20 @@ class HypercertExchangeClient {
8088
8234
  const spenderAddress = this.addresses.TRANSFER_MANAGER_V2;
8089
8235
  return setApprovalForAll(signer, collectionAddress, spenderAddress, approved, overrides);
8090
8236
  }
8237
+ /**
8238
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
8239
+ * @param collectionAddress Address of the collection to be approved
8240
+ * @param approved true to approve, false to revoke the approval (default to true)
8241
+ * @param safeAddress Address of the Safe to use
8242
+ * @returns Safe transaction hash
8243
+ */
8244
+ approveAllCollectionItemsSafe(safeAddress, collectionAddress, approved = true) {
8245
+ if (!this.walletClient) {
8246
+ throw new Error("No wallet client");
8247
+ }
8248
+ const safeTransactionBuilder = new SafeTransactionBuilder(this.walletClient, this.chainId, this.addresses);
8249
+ return safeTransactionBuilder.approveAllCollectionItems(safeAddress, collectionAddress, approved);
8250
+ }
8091
8251
  /**
8092
8252
  * Approve an ERC20 to be used as a currency on the Hypercert Exchange.
8093
8253
  * The spender is the HypercertExchangeProtocol contract.
@@ -8239,10 +8399,9 @@ class HypercertExchangeClient {
8239
8399
  if (!currency) {
8240
8400
  throw new ErrorCurrency();
8241
8401
  }
8242
- const chainId = this.chainId;
8243
8402
  const { nonce_counter } = await this.api.fetchOrderNonce({
8244
8403
  address,
8245
- chainId,
8404
+ chainId: this.chainId,
8246
8405
  });
8247
8406
  return this.createMakerAsk({
8248
8407
  // Defaults
@@ -8260,14 +8419,90 @@ class HypercertExchangeClient {
8260
8419
  additionalParameters,
8261
8420
  });
8262
8421
  }
8422
+ /**
8423
+ * Create a maker ask for a collection or singular offer of fractions using Safe
8424
+ * @param CreateDirectFractionsSaleMakerAskInput
8425
+ */
8426
+ async createDirectFractionsSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, additionalParameters = [], safeAddress, }) {
8427
+ if (!safeAddress) {
8428
+ throw new Error("Safe address is required for Safe transactions");
8429
+ }
8430
+ if (!currency) {
8431
+ throw new ErrorCurrency();
8432
+ }
8433
+ const { nonce_counter } = await this.api.fetchOrderNonce({
8434
+ address: safeAddress,
8435
+ chainId: this.chainId,
8436
+ });
8437
+ return this.createMakerAsk({
8438
+ // Defaults
8439
+ strategyId: exports.StrategyType.standard,
8440
+ collectionType: exports.CollectionType.HYPERCERT,
8441
+ collection: this.addresses.MINTER,
8442
+ subsetNonce: 0,
8443
+ currency,
8444
+ orderNonce: nonce_counter.toString(),
8445
+ // User specified
8446
+ itemIds,
8447
+ price,
8448
+ startTime,
8449
+ endTime,
8450
+ additionalParameters,
8451
+ safeAddress,
8452
+ });
8453
+ }
8263
8454
  /**
8264
8455
  * Create a maker ask to let the buyer decide how much of a fraction they want to buy
8265
8456
  * @param CreateFractionalSaleMakerInput
8266
8457
  */
8267
8458
  async createFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, }) {
8268
- const address = await this.signer?.getAddress();
8459
+ return this.prepareFractionalSaleMakerAsk({
8460
+ itemIds,
8461
+ price,
8462
+ startTime,
8463
+ endTime,
8464
+ currency,
8465
+ maxUnitAmount,
8466
+ minUnitAmount,
8467
+ minUnitsToKeep,
8468
+ sellLeftoverFraction,
8469
+ root,
8470
+ });
8471
+ }
8472
+ /**
8473
+ * Create a maker ask to let the buyer decide how much of a fraction they want to buy using Safe
8474
+ * @param CreateFractionalSaleMakerInput
8475
+ */
8476
+ async createFractionalSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }) {
8477
+ if (!safeAddress) {
8478
+ throw new Error("Safe address is required for Safe transactions");
8479
+ }
8480
+ return this.prepareFractionalSaleMakerAsk({
8481
+ itemIds,
8482
+ price,
8483
+ startTime,
8484
+ endTime,
8485
+ currency,
8486
+ maxUnitAmount,
8487
+ minUnitAmount,
8488
+ minUnitsToKeep,
8489
+ sellLeftoverFraction,
8490
+ root,
8491
+ safeAddress,
8492
+ });
8493
+ }
8494
+ /**
8495
+ * Prepare a fractional sale maker ask with common logic for both regular and Safe transactions
8496
+ * @param params CreateFractionalSaleMakerInput with optional safeAddress
8497
+ * @private
8498
+ */
8499
+ async prepareFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }) {
8500
+ let address = safeAddress;
8269
8501
  if (!address) {
8270
- throw new Error("No signer address could be determined");
8502
+ address = await this.getSigner().getAddress();
8503
+ if (!address) {
8504
+ throw new Error("No signer address could be determined");
8505
+ }
8271
8506
  }
8272
8507
  if (!currency) {
8273
8508
  throw new ErrorCurrency();
@@ -8291,6 +8526,7 @@ class HypercertExchangeClient {
8291
8526
  price,
8292
8527
  startTime,
8293
8528
  endTime,
8529
+ safeAddress,
8294
8530
  };
8295
8531
  if (root) {
8296
8532
  return this.createMakerAsk({
@@ -8337,6 +8573,16 @@ class HypercertExchangeClient {
8337
8573
  chainId,
8338
8574
  });
8339
8575
  }
8576
+ /**
8577
+ * Register the order with the hypercerts marketplace API for Safe transactions
8578
+ * @param messageHash The message hash from the Safe transaction
8579
+ */
8580
+ async registerOrderSafe({ messageHash }) {
8581
+ return this.api.registerOrderSafe({
8582
+ messageHash,
8583
+ chainId: this.chainId,
8584
+ });
8585
+ }
8340
8586
  /**
8341
8587
  * Delete the order from the hypercerts marketplace API
8342
8588
  * @param orderId Order ID
@@ -8350,8 +8596,11 @@ class HypercertExchangeClient {
8350
8596
  return this.api.deleteOrder(orderId, signedMessage);
8351
8597
  }
8352
8598
  /**
8353
- * Bundle approval operations into a single Safe transaction
8599
+ * Bundle the following operations into a single Safe transaction:
8600
+ * - grantApprovals on the TransferManager
8601
+ * - setApprovalForAll on the collection
8354
8602
  * @param safeAddress The address of the Safe contract
8603
+ * @param walletClient Connected wallet client
8355
8604
  * @param collectionAddress Address of the collection to approve
8356
8605
  * @returns Transaction hash
8357
8606
  */
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 = [
@@ -670,7 +670,7 @@ var strategies = /*#__PURE__*/Object.freeze({
670
670
  });
671
671
 
672
672
  /** All possible supported currencies */
673
- const SUPPORTED_CURRENCIES = ["ETH", "WETH", "DAI", "CELO", "cUSD", "USDT", "USDC"];
673
+ const SUPPORTED_CURRENCIES = ["ETH", "WETH", "DAI", "CELO", "cUSD", "USDT", "USDC", "USDGLO"];
674
674
  /** List of supported chains */
675
675
  var ChainId;
676
676
  (function (ChainId) {
@@ -1148,6 +1148,7 @@ const currencyAddressesPerChain = {
1148
1148
  cUSD: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
1149
1149
  USDC: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
1150
1150
  USDT: "0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e",
1151
+ USDGLO: "0x4F604735c1cF31399C6E711D5962b2B3E0225AD3",
1151
1152
  },
1152
1153
  [ChainId.ARBITRUM]: {
1153
1154
  ETH: ZeroAddress,
@@ -1203,6 +1204,11 @@ const getCurrencies = (chainId) => {
1203
1204
  address: currenciesForChain.USDT,
1204
1205
  decimals: 6,
1205
1206
  },
1207
+ USDGLO: {
1208
+ symbol: "USDGLO",
1209
+ address: currenciesForChain.USDGLO,
1210
+ decimals: 18,
1211
+ },
1206
1212
  };
1207
1213
  };
1208
1214
  const currenciesByNetwork = {
@@ -7638,6 +7644,7 @@ class ApiClient {
7638
7644
  },
7639
7645
  body: JSON.stringify({
7640
7646
  ...orderWithoutGlobalNonce,
7647
+ type: "eoa",
7641
7648
  globalNonce: globalNonce.toString(10),
7642
7649
  price: order.price.toString(10),
7643
7650
  quoteType,
@@ -7647,6 +7654,24 @@ class ApiClient {
7647
7654
  }),
7648
7655
  }).then((res) => this.handleResponse(res));
7649
7656
  };
7657
+ /**
7658
+ * Registers order in the marketplace API for Safe transactions
7659
+ * @param messageHash The message hash from the Safe transaction
7660
+ * @param chainId Chain ID
7661
+ */
7662
+ this.registerOrderSafe = async ({ messageHash, chainId, }) => {
7663
+ return fetch(`${this._baseUrl}/marketplace/orders/`, {
7664
+ method: "POST",
7665
+ headers: {
7666
+ "Content-Type": "application/json",
7667
+ },
7668
+ body: JSON.stringify({
7669
+ type: "multisig",
7670
+ messageHash,
7671
+ chainId,
7672
+ }),
7673
+ }).then((res) => this.handleResponse(res));
7674
+ };
7650
7675
  /**
7651
7676
  * @deprecated use GraphQL API instead
7652
7677
  * Fetch existing open orders from the marketplace API
@@ -7753,6 +7778,7 @@ class SafeTransactionBuilder {
7753
7778
  const nonce = await this.apiKit.getNextNonce(safeAddress);
7754
7779
  const safeTx = await connected.createTransaction({
7755
7780
  transactions,
7781
+ onlyCalls: true,
7756
7782
  options: {
7757
7783
  nonce,
7758
7784
  },
@@ -7824,7 +7850,107 @@ class SafeTransactionBuilder {
7824
7850
  }));
7825
7851
  return this.performSafeTransactions(safeAddress, transactions);
7826
7852
  }
7853
+ /**
7854
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
7855
+ * @internal
7856
+ */
7857
+ approveAllCollectionItems(safeAddress, collectionAddress, approved = true) {
7858
+ const erc721Contract = new Contract(collectionAddress, abiIERC721);
7859
+ const transactions = [
7860
+ {
7861
+ to: collectionAddress,
7862
+ data: erc721Contract.interface.encodeFunctionData("setApprovalForAll", [
7863
+ this.addresses.TRANSFER_MANAGER_V2,
7864
+ approved,
7865
+ ]),
7866
+ value: "0",
7867
+ },
7868
+ ];
7869
+ return this.performSafeTransactions(safeAddress, transactions);
7870
+ }
7871
+ }
7872
+
7873
+ class SafeMessages {
7874
+ /**
7875
+ * @param address - The address of the Safe
7876
+ * @param chainId - The chainId where the Safe is deployed
7877
+ * @param provider - An EIP1193 compatible provider
7878
+ * @param safeApiKit - A pre-initialized Safe API Kit e.g. when on a network with a 3rd party transaction service deployment
7879
+ */
7880
+ constructor(address, chainId, provider, safeApiKit) {
7881
+ this.address = address;
7882
+ this.chainId = chainId;
7883
+ this.provider = provider;
7884
+ this.apiKit = safeApiKit || new SafeApiKit({ chainId: BigInt(chainId) });
7885
+ }
7886
+ async signAndSubmit(values, types, primaryType) {
7887
+ try {
7888
+ const { messageHash } = await this.initiateSigning({
7889
+ types: {
7890
+ ...types,
7891
+ },
7892
+ primaryType,
7893
+ message: values,
7894
+ });
7895
+ return messageHash;
7896
+ }
7897
+ catch (error) {
7898
+ console.error("[signAndSubmit] error", error instanceof Error ? error.message : error);
7899
+ throw error instanceof Error ? error : new Error("Error signing and submitting message");
7900
+ }
7901
+ }
7902
+ async initiateSigning(config) {
7903
+ if (!this.chainId)
7904
+ throw new Error("No chainId found");
7905
+ const safe = await Safe.init({
7906
+ provider: this.provider,
7907
+ safeAddress: this.address,
7908
+ });
7909
+ const domain = {
7910
+ name: DOMAIN_NAME,
7911
+ version: DOMAIN_VERSION.toString(),
7912
+ chainId: parseInt(this.chainId.toString()),
7913
+ verifyingContract: this.address,
7914
+ };
7915
+ const typedData = {
7916
+ domain,
7917
+ types: {
7918
+ ...DOMAIN_TYPE,
7919
+ ...config.types,
7920
+ },
7921
+ primaryType: config.primaryType,
7922
+ message: config.message,
7923
+ };
7924
+ const safeMessage = await safe.createMessage(typedData);
7925
+ const signature = await safe.signTypedData(safeMessage);
7926
+ try {
7927
+ await this.apiKit.addMessage(this.address, {
7928
+ message: typedData,
7929
+ signature: buildSignatureBytes([signature]),
7930
+ });
7931
+ }
7932
+ catch (error) {
7933
+ console.error("Error adding message to Safe API:", error);
7934
+ throw error;
7935
+ }
7936
+ const rawMessageHash = this.calculateMessageHash(domain, config);
7937
+ const safeHash = await safe.getSafeMessageHash(rawMessageHash);
7938
+ return {
7939
+ messageHash: safeHash,
7940
+ };
7941
+ }
7942
+ calculateMessageHash(domain, config) {
7943
+ return TypedDataEncoder.hash(domain, config.types, config.message);
7944
+ }
7827
7945
  }
7946
+ const DOMAIN_TYPE = {
7947
+ EIP712Domain: [
7948
+ { name: "name", type: "string" },
7949
+ { name: "version", type: "string" },
7950
+ { name: "chainId", type: "uint256" },
7951
+ { name: "verifyingContract", type: "address" },
7952
+ ],
7953
+ };
7828
7954
 
7829
7955
  /**
7830
7956
  * HypercertExchange
@@ -7888,12 +8014,12 @@ class HypercertExchangeClient {
7888
8014
  * @param CreateMakerInput
7889
8015
  * @returns the maker object, isTransferManagerApproved, and isTransferManagerApproved
7890
8016
  */
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();
8017
+ async createMakerAsk({ collection, strategyId, collectionType, subsetNonce, orderNonce, endTime, price, itemIds, currency = ZeroAddress, startTime = Math.floor(Date.now() / 1000), additionalParameters = [], safeAddress, }) {
7893
8018
  if (!this.isTimestampValid(startTime) || !this.isTimestampValid(endTime)) {
7894
8019
  throw new ErrorTimestamp();
7895
8020
  }
7896
- const signerAddress = await signer.getAddress();
8021
+ // Use safeAddress if provided, otherwise get from signer
8022
+ const signerAddress = safeAddress || await this.getSigner().getAddress();
7897
8023
  const spenderAddress = this.addresses.TRANSFER_MANAGER_V2;
7898
8024
  // Use this.provider (MulticallProvider) in order to batch the calls
7899
8025
  const [isCollectionApproved, userBidAskNonce, isTransferManagerApproved] = await Promise.all([
@@ -7990,6 +8116,26 @@ class HypercertExchangeClient {
7990
8116
  const signer = this.getSigner();
7991
8117
  return await signMakerOrder(signer, this.getTypedDataDomain(), maker);
7992
8118
  }
8119
+ /**
8120
+ * Create a maker order message and upload it to the Safe Transaction Service to be signed in the Safe app
8121
+ * @param maker Order to be signed by the user
8122
+ * @param safeAddress Address of the Safe to use
8123
+ * @param safeApiKit Optional pre-initialized Safe API Kit instance
8124
+ * @returns Signature
8125
+ */
8126
+ async signMakerOrderSafe(maker, safeAddress, safeApiKit) {
8127
+ if (!this.walletClient) {
8128
+ throw new Error("wallet client is required to sign a maker order using Safe");
8129
+ }
8130
+ const safe = new SafeMessages(safeAddress, this.chainId,
8131
+ // The underlying provider is an Eip1193Provider, but the type is not exported
8132
+ this.walletClient, safeApiKit);
8133
+ // The assertion to unknown and Record<string, unknown> is necessary because Maker is a closed type while
8134
+ // Record<string, unknown> is not. Thus TypeScript will complain about the types having no overlap. But we know
8135
+ // that Maker is a Record<string, unknown> and sign() is not trying to get keys out of the Record that don't
8136
+ // exist on Maker, so we can safely type assert here.
8137
+ return safe.signAndSubmit(maker, makerTypes, "Maker");
8138
+ }
7993
8139
  /**
7994
8140
  * Sign multiple maker orders with a single signature
7995
8141
  * /!\ Use this function for UI implementation only
@@ -8086,6 +8232,20 @@ class HypercertExchangeClient {
8086
8232
  const spenderAddress = this.addresses.TRANSFER_MANAGER_V2;
8087
8233
  return setApprovalForAll(signer, collectionAddress, spenderAddress, approved, overrides);
8088
8234
  }
8235
+ /**
8236
+ * Approve all items in a collection for trading on the Hypercert Exchange using Safe
8237
+ * @param collectionAddress Address of the collection to be approved
8238
+ * @param approved true to approve, false to revoke the approval (default to true)
8239
+ * @param safeAddress Address of the Safe to use
8240
+ * @returns Safe transaction hash
8241
+ */
8242
+ approveAllCollectionItemsSafe(safeAddress, collectionAddress, approved = true) {
8243
+ if (!this.walletClient) {
8244
+ throw new Error("No wallet client");
8245
+ }
8246
+ const safeTransactionBuilder = new SafeTransactionBuilder(this.walletClient, this.chainId, this.addresses);
8247
+ return safeTransactionBuilder.approveAllCollectionItems(safeAddress, collectionAddress, approved);
8248
+ }
8089
8249
  /**
8090
8250
  * Approve an ERC20 to be used as a currency on the Hypercert Exchange.
8091
8251
  * The spender is the HypercertExchangeProtocol contract.
@@ -8237,10 +8397,9 @@ class HypercertExchangeClient {
8237
8397
  if (!currency) {
8238
8398
  throw new ErrorCurrency();
8239
8399
  }
8240
- const chainId = this.chainId;
8241
8400
  const { nonce_counter } = await this.api.fetchOrderNonce({
8242
8401
  address,
8243
- chainId,
8402
+ chainId: this.chainId,
8244
8403
  });
8245
8404
  return this.createMakerAsk({
8246
8405
  // Defaults
@@ -8258,14 +8417,90 @@ class HypercertExchangeClient {
8258
8417
  additionalParameters,
8259
8418
  });
8260
8419
  }
8420
+ /**
8421
+ * Create a maker ask for a collection or singular offer of fractions using Safe
8422
+ * @param CreateDirectFractionsSaleMakerAskInput
8423
+ */
8424
+ async createDirectFractionsSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, additionalParameters = [], safeAddress, }) {
8425
+ if (!safeAddress) {
8426
+ throw new Error("Safe address is required for Safe transactions");
8427
+ }
8428
+ if (!currency) {
8429
+ throw new ErrorCurrency();
8430
+ }
8431
+ const { nonce_counter } = await this.api.fetchOrderNonce({
8432
+ address: safeAddress,
8433
+ chainId: this.chainId,
8434
+ });
8435
+ return this.createMakerAsk({
8436
+ // Defaults
8437
+ strategyId: StrategyType.standard,
8438
+ collectionType: CollectionType.HYPERCERT,
8439
+ collection: this.addresses.MINTER,
8440
+ subsetNonce: 0,
8441
+ currency,
8442
+ orderNonce: nonce_counter.toString(),
8443
+ // User specified
8444
+ itemIds,
8445
+ price,
8446
+ startTime,
8447
+ endTime,
8448
+ additionalParameters,
8449
+ safeAddress,
8450
+ });
8451
+ }
8261
8452
  /**
8262
8453
  * Create a maker ask to let the buyer decide how much of a fraction they want to buy
8263
8454
  * @param CreateFractionalSaleMakerInput
8264
8455
  */
8265
8456
  async createFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, }) {
8266
- const address = await this.signer?.getAddress();
8457
+ return this.prepareFractionalSaleMakerAsk({
8458
+ itemIds,
8459
+ price,
8460
+ startTime,
8461
+ endTime,
8462
+ currency,
8463
+ maxUnitAmount,
8464
+ minUnitAmount,
8465
+ minUnitsToKeep,
8466
+ sellLeftoverFraction,
8467
+ root,
8468
+ });
8469
+ }
8470
+ /**
8471
+ * Create a maker ask to let the buyer decide how much of a fraction they want to buy using Safe
8472
+ * @param CreateFractionalSaleMakerInput
8473
+ */
8474
+ async createFractionalSaleMakerAskSafe({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }) {
8475
+ if (!safeAddress) {
8476
+ throw new Error("Safe address is required for Safe transactions");
8477
+ }
8478
+ return this.prepareFractionalSaleMakerAsk({
8479
+ itemIds,
8480
+ price,
8481
+ startTime,
8482
+ endTime,
8483
+ currency,
8484
+ maxUnitAmount,
8485
+ minUnitAmount,
8486
+ minUnitsToKeep,
8487
+ sellLeftoverFraction,
8488
+ root,
8489
+ safeAddress,
8490
+ });
8491
+ }
8492
+ /**
8493
+ * Prepare a fractional sale maker ask with common logic for both regular and Safe transactions
8494
+ * @param params CreateFractionalSaleMakerInput with optional safeAddress
8495
+ * @private
8496
+ */
8497
+ async prepareFractionalSaleMakerAsk({ itemIds, price, startTime, endTime, currency, maxUnitAmount, minUnitAmount, minUnitsToKeep, sellLeftoverFraction, root, safeAddress, }) {
8498
+ let address = safeAddress;
8267
8499
  if (!address) {
8268
- throw new Error("No signer address could be determined");
8500
+ address = await this.getSigner().getAddress();
8501
+ if (!address) {
8502
+ throw new Error("No signer address could be determined");
8503
+ }
8269
8504
  }
8270
8505
  if (!currency) {
8271
8506
  throw new ErrorCurrency();
@@ -8289,6 +8524,7 @@ class HypercertExchangeClient {
8289
8524
  price,
8290
8525
  startTime,
8291
8526
  endTime,
8527
+ safeAddress,
8292
8528
  };
8293
8529
  if (root) {
8294
8530
  return this.createMakerAsk({
@@ -8335,6 +8571,16 @@ class HypercertExchangeClient {
8335
8571
  chainId,
8336
8572
  });
8337
8573
  }
8574
+ /**
8575
+ * Register the order with the hypercerts marketplace API for Safe transactions
8576
+ * @param messageHash The message hash from the Safe transaction
8577
+ */
8578
+ async registerOrderSafe({ messageHash }) {
8579
+ return this.api.registerOrderSafe({
8580
+ messageHash,
8581
+ chainId: this.chainId,
8582
+ });
8583
+ }
8338
8584
  /**
8339
8585
  * Delete the order from the hypercerts marketplace API
8340
8586
  * @param orderId Order ID
@@ -8348,8 +8594,11 @@ class HypercertExchangeClient {
8348
8594
  return this.api.deleteOrder(orderId, signedMessage);
8349
8595
  }
8350
8596
  /**
8351
- * Bundle approval operations into a single Safe transaction
8597
+ * Bundle the following operations into a single Safe transaction:
8598
+ * - grantApprovals on the TransferManager
8599
+ * - setApprovalForAll on the collection
8352
8600
  * @param safeAddress The address of the Safe contract
8601
+ * @param walletClient Connected wallet client
8353
8602
  * @param collectionAddress Address of the collection to approve
8354
8603
  * @returns Transaction hash
8355
8604
  */
@@ -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
package/dist/types.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface Currency {
14
14
  decimals: number;
15
15
  }
16
16
  /** All possible supported currencies */
17
- export declare const SUPPORTED_CURRENCIES: readonly ["ETH", "WETH", "DAI", "CELO", "cUSD", "USDT", "USDC"];
17
+ export declare const SUPPORTED_CURRENCIES: readonly ["ETH", "WETH", "DAI", "CELO", "cUSD", "USDT", "USDC", "USDGLO"];
18
18
  export type SupportedCurrencySymbol = (typeof SUPPORTED_CURRENCIES)[number];
19
19
  /** Type for currency configuration */
20
20
  export type Currencies = Partial<Record<SupportedCurrencySymbol, Currency>>;
@@ -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.8.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": {