@cloakely/sdk 0.8.3 → 0.8.5

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.
@@ -60,6 +60,8 @@ export interface CloakDeployment {
60
60
  dropFactoryBlock?: bigint;
61
61
  /** CloakSpotBook: WL spot sales on top of drops; winners name the wallets to whitelist. */
62
62
  spotBook?: Address;
63
+ /** CloakEscrowDropFactory: sealed auctions of NFTs that already exist (listed with the drops). */
64
+ escrowDropFactory?: Address;
63
65
  hypePool: Address;
64
66
  whype: Address;
65
67
  /** Hosted relayer (POST /relay, GET /quote). */
@@ -25,18 +25,20 @@ export const deployments = {
25
25
  shared: "0x0ce2ecb0195e07899377e3d7b01e927ab3f87610",
26
26
  sharedBlock: 21426599n,
27
27
  sharedRouter: "0xe2a0769a5acbd987c1b6725247c56187a112dc7e",
28
- sharedBatcher: "0xdc725bdeb0fca923bf2dc65232f54463b0f80820",
28
+ sharedBatcher: "0x11598b35fabc90fdd45d8a77ac41e2057c32ea14",
29
29
  // sealed NFT drops (Nov 3): GTD, then a sealed auction with the creator's price as the floor and a WL bid boost
30
30
  dropFactory: "0x2afb7d9a5baa02a2d72a7c761521fc66dce9d783",
31
31
  dropFactoryBlock: 22329730n,
32
32
  spotBook: "0x91806d96814cc205d4b642e280023a1c7b361fc8",
33
+ escrowDropFactory: "0x5f3efbfd5d15440947abfbfce7f2fcfffff71079",
33
34
  router: "0x343ab9a7aac93cee28321410510ea24417cc6386",
34
35
  routerBlock: 19485063n,
35
- batcher: "0xc69e4bca18020ae93bc9f17093985bfb139a3203",
36
+ // batchers replaced Nov 6: the crossing price stays within 2% of the price when a round opened
37
+ batcher: "0x54a503b0683af40aa3427b99292018f4d045d43f",
36
38
  migrator: "0x86c5d8035224493f10938aaa07b179be316dadf4",
37
39
  hypePool: "0x3ACB22829bb422446caaDd8546e65ab530A53031",
38
40
  whype: "0xe44b27113D4b947c0B1bDf4a93878D0e115f4bdD",
39
- relayerUrl: "https://cloak-keeper.onrender.com",
41
+ relayerUrl: "https://keeper.boblabs.tech",
40
42
  circuitsUrl: "https://cloak.boblabs.tech/circuits",
41
43
  assets: [
42
44
  // Elysium testnet USDC (bridged from Circle's USDC on HyperEVM): private sends and withdrawals
package/dist/drops.d.ts CHANGED
@@ -18,6 +18,102 @@ export declare const DROP_MAX_PER_BID = 20;
18
18
  export declare const DROP_MAX_SUPPLY = 10000;
19
19
  /** Most entries one GTD or WL list can hold (the lists travel in one transaction). */
20
20
  export declare const DROP_LIST_MAX = 2000;
21
+ /** Most bytes a drop's published details (images, links, lists) can take: CloakDropFactory.MAX_DETAILS. */
22
+ export declare const DROP_MAX_DETAILS = 96000;
23
+ /** Most existing NFTs one escrow drop can auction (they all move in when it's created). */
24
+ export declare const ESCROW_MAX_TOKENS = 500;
25
+ declare const ERC721_ABI: readonly [{
26
+ readonly type: "function";
27
+ readonly name: "ownerOf";
28
+ readonly stateMutability: "view";
29
+ readonly inputs: readonly [{
30
+ readonly type: "uint256";
31
+ }];
32
+ readonly outputs: readonly [{
33
+ readonly type: "address";
34
+ }];
35
+ }, {
36
+ readonly type: "function";
37
+ readonly name: "isApprovedForAll";
38
+ readonly stateMutability: "view";
39
+ readonly inputs: readonly [{
40
+ readonly type: "address";
41
+ }, {
42
+ readonly type: "address";
43
+ }];
44
+ readonly outputs: readonly [{
45
+ readonly type: "bool";
46
+ }];
47
+ }, {
48
+ readonly type: "function";
49
+ readonly name: "setApprovalForAll";
50
+ readonly stateMutability: "nonpayable";
51
+ readonly inputs: readonly [{
52
+ readonly type: "address";
53
+ }, {
54
+ readonly type: "bool";
55
+ }];
56
+ readonly outputs: readonly [];
57
+ }, {
58
+ readonly type: "function";
59
+ readonly name: "tokenURI";
60
+ readonly stateMutability: "view";
61
+ readonly inputs: readonly [{
62
+ readonly type: "uint256";
63
+ }];
64
+ readonly outputs: readonly [{
65
+ readonly type: "string";
66
+ }];
67
+ }, {
68
+ readonly type: "function";
69
+ readonly name: "name";
70
+ readonly stateMutability: "view";
71
+ readonly inputs: readonly [];
72
+ readonly outputs: readonly [{
73
+ readonly type: "string";
74
+ }];
75
+ }, {
76
+ readonly type: "function";
77
+ readonly name: "symbol";
78
+ readonly stateMutability: "view";
79
+ readonly inputs: readonly [];
80
+ readonly outputs: readonly [{
81
+ readonly type: "string";
82
+ }];
83
+ }, {
84
+ readonly type: "function";
85
+ readonly name: "balanceOf";
86
+ readonly stateMutability: "view";
87
+ readonly inputs: readonly [{
88
+ readonly type: "address";
89
+ }];
90
+ readonly outputs: readonly [{
91
+ readonly type: "uint256";
92
+ }];
93
+ }, {
94
+ readonly type: "function";
95
+ readonly name: "tokenOfOwnerByIndex";
96
+ readonly stateMutability: "view";
97
+ readonly inputs: readonly [{
98
+ readonly type: "address";
99
+ }, {
100
+ readonly type: "uint256";
101
+ }];
102
+ readonly outputs: readonly [{
103
+ readonly type: "uint256";
104
+ }];
105
+ }, {
106
+ readonly type: "function";
107
+ readonly name: "supportsInterface";
108
+ readonly stateMutability: "view";
109
+ readonly inputs: readonly [{
110
+ readonly type: "bytes4";
111
+ }];
112
+ readonly outputs: readonly [{
113
+ readonly type: "bool";
114
+ }];
115
+ }];
116
+ export { ERC721_ABI as erc721Abi };
21
117
  /** How long the keeper has to open the bids at most. Settlement doesn't wait for it once every bid is open. */
22
118
  export declare const DROP_DEFAULT_REVEAL_SECS = 1800;
23
119
  /** A WL list entry, as the drop hashes it. */
@@ -33,9 +129,23 @@ export interface GtdEntry {
33
129
  wallet: Address;
34
130
  spots: number;
35
131
  }
132
+ export interface DropLinks {
133
+ website?: string;
134
+ x?: string;
135
+ discord?: string;
136
+ telegram?: string;
137
+ }
138
+ /** Keep only https links, at most 200 characters each. */
139
+ export declare function cleanLinks(l: unknown): DropLinks | undefined;
36
140
  /** What a drop publishes about itself at creation (the DropDetails event). */
37
141
  export interface DropDetails {
38
142
  image?: string;
143
+ /** A wide banner for the drop page (an https:, ipfs: or small data: image). */
144
+ banner?: string;
145
+ /** Where the collection lives online. Only https links are kept. */
146
+ links?: DropLinks;
147
+ /** A category shown with the collection, like Art or PFP. */
148
+ category?: string;
39
149
  description?: string;
40
150
  gtd?: GtdEntry[];
41
151
  wl?: Address[];
@@ -72,12 +182,52 @@ export interface CreateDropOptions {
72
182
  * as every bid is open, so a long window only matters if the keeper is down; a bid still sealed at its end loses 10%. */
73
183
  revealSecs?: number;
74
184
  image?: string;
185
+ /** A wide banner for the drop page (an https:, ipfs: or small data: image). */
186
+ banner?: string;
187
+ /** Where the collection lives online. Only https links are kept. */
188
+ links?: DropLinks;
189
+ /** A category shown with the collection, like Art or PFP. */
190
+ category?: string;
75
191
  description?: string;
76
192
  /** A WL spot sale: published with the drop so its pages and metadata say so. */
77
193
  spots?: {
78
194
  partner: string;
79
195
  };
80
196
  }
197
+ /** An auction of NFTs that already exist (CloakEscrowDrop): the tokens move into the drop when it's created. */
198
+ export interface CreateEscrowDropOptions {
199
+ /** The ERC-721 collection the tokens belong to. */
200
+ collection: Address;
201
+ /** The token ids to auction, owned by the caller (1 to 500). */
202
+ tokenIds: bigint[];
203
+ /** The auction's name, like "ELYMPUS treasury auction". */
204
+ name: string;
205
+ symbol: string;
206
+ floor: bigint;
207
+ tickSize?: bigint;
208
+ numTicks?: number;
209
+ maxPerBid?: number;
210
+ wl?: {
211
+ list: Address[];
212
+ boostBps: number;
213
+ };
214
+ biddingSecs: number;
215
+ revealSecs?: number;
216
+ image?: string;
217
+ banner?: string;
218
+ links?: DropLinks;
219
+ category?: string;
220
+ description?: string;
221
+ }
222
+ /** Where an escrow drop's tokens come from. */
223
+ export interface DropEscrow {
224
+ collection: Address;
225
+ tokenIds: bigint[];
226
+ /** How many of them the drop holds right now. */
227
+ held: number;
228
+ /** Unsold tokens already returned to the owner. */
229
+ returned: number;
230
+ }
81
231
  export type DropPhase = "gtd" | "bidding" | "reveal" | "settle" | "numbering" | "settled";
82
232
  export interface DropState {
83
233
  address: Address;
@@ -113,6 +263,8 @@ export interface DropState {
113
263
  wlPrice: bigint;
114
264
  tieSupply: number;
115
265
  tieDemand: number;
266
+ /** Set for an auction of existing NFTs (CloakEscrowDrop). */
267
+ escrow?: DropEscrow;
116
268
  }
117
269
  export interface DropBid {
118
270
  id: bigint;
@@ -135,9 +287,14 @@ export declare class CloakDrops {
135
287
  readonly factory: Address;
136
288
  /** First block to read the factory's and drops' events from. */
137
289
  readonly fromBlock: bigint;
290
+ /** CloakEscrowDropFactory: auctions of NFTs that already exist, listed next to the drops. */
291
+ readonly escrowFactory?: Address | undefined;
138
292
  constructor(publicClient: PublicClient, walletClient: Wallet | undefined, factory: Address,
139
293
  /** First block to read the factory's and drops' events from. */
140
- fromBlock?: bigint);
294
+ fromBlock?: bigint,
295
+ /** CloakEscrowDropFactory: auctions of NFTs that already exist, listed next to the drops. */
296
+ escrowFactory?: Address | undefined);
297
+ private get factories();
141
298
  /** Many reads in one multicall where the chain has multicall3, else one by one (a local chain). */
142
299
  private reads;
143
300
  private wallet;
@@ -152,6 +309,30 @@ export declare class CloakDrops {
152
309
  /** A drop's published details: its image, description and the lists behind its roots. */
153
310
  details(drop: Address): Promise<DropDetails>;
154
311
  get(drop: Address): Promise<DropState>;
312
+ /** Where an escrow drop's tokens come from, or undefined for a drop that mints its own. */
313
+ escrowOf(drop: Address): Promise<DropEscrow | undefined>;
314
+ /** The token ids a bid won in an escrow drop (once settled and numbered). */
315
+ tokensOf(drop: Address, bidId: bigint): Promise<bigint[]>;
316
+ /**
317
+ * Auction NFTs that already exist. Needs the escrow factory. Checks the caller owns every token, approves the
318
+ * factory for the collection if it isn't yet (one transaction), then creates the auction, which moves every token
319
+ * into it. Returns the drop's address.
320
+ */
321
+ createEscrow(o: CreateEscrowDropOptions): Promise<{
322
+ drop: Address;
323
+ txHash: Hex;
324
+ approvalTx?: Hex;
325
+ }>;
326
+ /**
327
+ * The token ids of `collection` a wallet holds, from the collection's Transfer events (works for any ERC-721, no
328
+ * enumeration needed). Capped at `max`.
329
+ */
330
+ ownedTokens(collection: Address, owner: Address, opts?: {
331
+ max?: number;
332
+ fromBlock?: bigint;
333
+ }): Promise<bigint[]>;
334
+ /** The block a contract was deployed in, by binary search on its code (0 if the node can't tell). */
335
+ private deployBlock;
155
336
  /** The bid level for a price: the highest level at or below it (bids snap down, never up). */
156
337
  static tickFor(s: Pick<DropState, "floor" | "tickSize" | "numTicks">, price: bigint): number;
157
338
  static priceAt(s: Pick<DropState, "floor" | "tickSize">, tick: number): bigint;
package/dist/drops.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * DropDetails event, so anyone can rebuild a proof; a list that doesn't match the root proves nothing.
12
12
  */
13
13
  import { encodeAbiParameters, getAddress, isAddress, keccak256, toHex } from "viem";
14
- import { cloakDropAbi, cloakDropFactoryAbi } from "./abis.js";
14
+ import { cloakDropAbi, cloakDropFactoryAbi, cloakEscrowDropAbi, cloakEscrowDropFactoryAbi } from "./abis.js";
15
15
  import { commitmentOf } from "./cloak.js";
16
16
  import { drandRoundAt, drandRoundTime, sealBid, unsealBid } from "./seal.js";
17
17
  export const DROP_BPS = 10000n;
@@ -20,6 +20,22 @@ export const DROP_MAX_PER_BID = 20;
20
20
  export const DROP_MAX_SUPPLY = 10_000;
21
21
  /** Most entries one GTD or WL list can hold (the lists travel in one transaction). */
22
22
  export const DROP_LIST_MAX = 2_000;
23
+ /** Most bytes a drop's published details (images, links, lists) can take: CloakDropFactory.MAX_DETAILS. */
24
+ export const DROP_MAX_DETAILS = 96_000;
25
+ /** Most existing NFTs one escrow drop can auction (they all move in when it's created). */
26
+ export const ESCROW_MAX_TOKENS = 500;
27
+ const ERC721_ABI = [
28
+ { type: "function", name: "ownerOf", stateMutability: "view", inputs: [{ type: "uint256" }], outputs: [{ type: "address" }] },
29
+ { type: "function", name: "isApprovedForAll", stateMutability: "view", inputs: [{ type: "address" }, { type: "address" }], outputs: [{ type: "bool" }] },
30
+ { type: "function", name: "setApprovalForAll", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "bool" }], outputs: [] },
31
+ { type: "function", name: "tokenURI", stateMutability: "view", inputs: [{ type: "uint256" }], outputs: [{ type: "string" }] },
32
+ { type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
33
+ { type: "function", name: "symbol", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
34
+ { type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
35
+ { type: "function", name: "tokenOfOwnerByIndex", stateMutability: "view", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "uint256" }] },
36
+ { type: "function", name: "supportsInterface", stateMutability: "view", inputs: [{ type: "bytes4" }], outputs: [{ type: "bool" }] },
37
+ ];
38
+ export { ERC721_ABI as erc721Abi };
23
39
  /** How long the keeper has to open the bids at most. Settlement doesn't wait for it once every bid is open. */
24
40
  export const DROP_DEFAULT_REVEAL_SECS = 1_800;
25
41
  // ───────────────────────────── Merkle lists ─────────────────────────────
@@ -59,6 +75,20 @@ export function merkleTree(leaves) {
59
75
  },
60
76
  };
61
77
  }
78
+ const LINK_KEYS = ["website", "x", "discord", "telegram"];
79
+ /** Keep only https links, at most 200 characters each. */
80
+ export function cleanLinks(l) {
81
+ if (!l || typeof l !== "object")
82
+ return undefined;
83
+ const out = {};
84
+ for (const k of LINK_KEYS) {
85
+ const v = l[k];
86
+ if (typeof v === "string" && /^https:\/\/[^\s"<>]+$/.test(v.trim()) && v.trim().length <= 200)
87
+ out[k] = v.trim();
88
+ }
89
+ return Object.keys(out).length ? out : undefined;
90
+ }
91
+ const imageOk = (v) => typeof v === "string" && /^(https:\/\/|ipfs:\/\/|data:image\/(png|jpeg|webp|gif);base64,)/.test(v);
62
92
  const PHASES = ["gtd", "bidding", "reveal", "settle", "numbering", "settled"];
63
93
  // ───────────────────────────── client ─────────────────────────────
64
94
  export class CloakDrops {
@@ -66,13 +96,20 @@ export class CloakDrops {
66
96
  walletClient;
67
97
  factory;
68
98
  fromBlock;
99
+ escrowFactory;
69
100
  constructor(publicClient, walletClient, factory,
70
101
  /** First block to read the factory's and drops' events from. */
71
- fromBlock = 0n) {
102
+ fromBlock = 0n,
103
+ /** CloakEscrowDropFactory: auctions of NFTs that already exist, listed next to the drops. */
104
+ escrowFactory) {
72
105
  this.publicClient = publicClient;
73
106
  this.walletClient = walletClient;
74
107
  this.factory = factory;
75
108
  this.fromBlock = fromBlock;
109
+ this.escrowFactory = escrowFactory;
110
+ }
111
+ get factories() {
112
+ return this.escrowFactory ? [this.factory, this.escrowFactory] : [this.factory];
76
113
  }
77
114
  /** Many reads in one multicall where the chain has multicall3, else one by one (a local chain). */
78
115
  async reads(contracts) {
@@ -95,10 +132,13 @@ export class CloakDrops {
95
132
  }
96
133
  /** Every drop the factory made, newest last. */
97
134
  async list() {
98
- const n = Number(await this.publicClient.readContract({ address: this.factory, abi: cloakDropFactoryAbi, functionName: "dropsCount" }));
99
- if (!n)
100
- return [];
101
- return (await this.reads(Array.from({ length: n }, (_, i) => ({ address: this.factory, abi: cloakDropFactoryAbi, functionName: "drops", args: [BigInt(i)] }))));
135
+ const out = [];
136
+ for (const f of this.factories) {
137
+ const n = Number(await this.publicClient.readContract({ address: f, abi: cloakDropFactoryAbi, functionName: "dropsCount" }));
138
+ if (n)
139
+ out.push(...(await this.reads(Array.from({ length: n }, (_, i) => ({ address: f, abi: cloakDropFactoryAbi, functionName: "drops", args: [BigInt(i)] })))));
140
+ }
141
+ return out;
102
142
  }
103
143
  /** Create a drop. The caller is its creator. Returns the drop's address. */
104
144
  async create(o) {
@@ -144,12 +184,16 @@ export class CloakDrops {
144
184
  gtdRoot: merkleTree(gtdList.map((e) => gtdLeaf(e.wallet, e.spots))).root,
145
185
  wlRoot: merkleTree(wlList.map(wlLeaf)).root,
146
186
  };
147
- const details = { v: 1, ...(o.image ? { image: o.image } : {}), ...(o.description ? { description: o.description.slice(0, 1000) } : {}), ...(gtdList.length ? { gtd: gtdList } : {}), ...(wlList.length ? { wl: wlList } : {}), ...(o.spots ? { spots: { partner: o.spots.partner.slice(0, 200) } } : {}) };
187
+ const links = cleanLinks(o.links);
188
+ const details = { v: 1, ...(o.image ? { image: o.image } : {}), ...(o.banner && imageOk(o.banner) ? { banner: o.banner } : {}), ...(links ? { links } : {}), ...(o.category?.trim() ? { category: o.category.trim().slice(0, 32) } : {}), ...(o.description ? { description: o.description.slice(0, 1000) } : {}), ...(gtdList.length ? { gtd: gtdList } : {}), ...(wlList.length ? { wl: wlList } : {}), ...(o.spots ? { spots: { partner: o.spots.partner.slice(0, 200) } } : {}) };
189
+ const detailsJson = JSON.stringify(details);
190
+ if (new TextEncoder().encode(detailsJson).length > DROP_MAX_DETAILS)
191
+ throw new Error("The images and lists are too big to publish together. Link the banner (https:// or ipfs://) instead of uploading it, or use a smaller image.");
148
192
  const { result, request } = await this.publicClient.simulateContract({
149
193
  address: this.factory,
150
194
  abi: cloakDropFactoryAbi,
151
195
  functionName: "createDrop",
152
- args: [params, o.name.trim(), o.symbol.trim(), o.baseURI.trim(), JSON.stringify(details)],
196
+ args: [params, o.name.trim(), o.symbol.trim(), o.baseURI.trim(), detailsJson],
153
197
  account: w.account,
154
198
  });
155
199
  const txHash = await w.writeContract(request);
@@ -158,11 +202,14 @@ export class CloakDrops {
158
202
  }
159
203
  /** A drop's published details: its image, description and the lists behind its roots. */
160
204
  async details(drop) {
161
- const logs = await this.chunked((from, to) => this.publicClient.getContractEvents({ address: this.factory, abi: cloakDropFactoryAbi, eventName: "DropDetails", args: { drop }, fromBlock: from, toBlock: to, strict: true }), true);
205
+ const logs = await this.chunked((from, to) => this.publicClient.getContractEvents({ address: this.factories, abi: cloakDropFactoryAbi, eventName: "DropDetails", args: { drop }, fromBlock: from, toBlock: to, strict: true }), true);
162
206
  try {
163
207
  const d = JSON.parse(logs[0]?.args.details ?? "{}");
164
208
  return {
165
209
  image: typeof d.image === "string" ? d.image : undefined,
210
+ banner: imageOk(d.banner) ? d.banner : undefined,
211
+ links: cleanLinks(d.links),
212
+ category: typeof d.category === "string" ? d.category.slice(0, 32) : undefined,
166
213
  description: typeof d.description === "string" ? d.description : undefined,
167
214
  gtd: Array.isArray(d.gtd) ? d.gtd.filter((e) => isAddress(e?.wallet) && Number.isInteger(e?.spots)) : undefined,
168
215
  wl: Array.isArray(d.wl) ? d.wl.filter((a) => isAddress(a)) : undefined,
@@ -218,7 +265,154 @@ export class CloakDrops {
218
265
  wlPrice,
219
266
  tieSupply: Number(r[14]),
220
267
  tieDemand: Number(r[15]),
268
+ escrow: await this.escrowOf(drop),
269
+ };
270
+ }
271
+ /** Where an escrow drop's tokens come from, or undefined for a drop that mints its own. */
272
+ async escrowOf(drop) {
273
+ const c = { address: drop, abi: cloakEscrowDropAbi };
274
+ const collection = await this.publicClient.readContract({ ...c, functionName: "collection" }).catch(() => undefined);
275
+ if (!collection)
276
+ return undefined;
277
+ const [tokenIds, held, returned] = await Promise.all([this.publicClient.readContract({ ...c, functionName: "tokenIds" }), this.publicClient.readContract({ ...c, functionName: "held" }), this.publicClient.readContract({ ...c, functionName: "unsoldMinted" })]);
278
+ return { collection, tokenIds: [...tokenIds], held: Number(held), returned: Number(returned) };
279
+ }
280
+ /** The token ids a bid won in an escrow drop (once settled and numbered). */
281
+ async tokensOf(drop, bidId) {
282
+ return [...(await this.publicClient.readContract({ address: drop, abi: cloakEscrowDropAbi, functionName: "tokensOf", args: [bidId] }))];
283
+ }
284
+ /**
285
+ * Auction NFTs that already exist. Needs the escrow factory. Checks the caller owns every token, approves the
286
+ * factory for the collection if it isn't yet (one transaction), then creates the auction, which moves every token
287
+ * into it. Returns the drop's address.
288
+ */
289
+ async createEscrow(o) {
290
+ const w = this.wallet();
291
+ if (!this.escrowFactory)
292
+ throw new Error("Auctions of existing NFTs aren't available on this network yet.");
293
+ if (!o.name.trim() || o.name.length > 64 || !o.symbol.trim() || o.symbol.length > 16)
294
+ throw new Error("Give the auction a name (up to 64 characters) and a symbol (up to 16).");
295
+ if (!isAddress(o.collection))
296
+ throw new Error("Enter the collection's contract address.");
297
+ const ids = [...new Set(o.tokenIds.map((x) => BigInt(x)))].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
298
+ if (!ids.length || ids.length > ESCROW_MAX_TOKENS)
299
+ throw new Error(`List 1 to ${ESCROW_MAX_TOKENS} tokens.`);
300
+ if (o.floor <= 0n)
301
+ throw new Error("Set a price above zero.");
302
+ const boost = o.wl?.boostBps ?? 0;
303
+ if (boost < 0 || boost > DROP_MAX_BOOST_BPS)
304
+ throw new Error("The WL boost is 0% to 30%.");
305
+ if ((o.wl?.list.length ?? 0) > DROP_LIST_MAX)
306
+ throw new Error(`A list holds up to ${DROP_LIST_MAX} wallets.`);
307
+ const nft = { address: getAddress(o.collection), abi: ERC721_ABI };
308
+ const me = w.account.address.toLowerCase();
309
+ const owners = await this.reads(ids.map((id) => ({ ...nft, functionName: "ownerOf", args: [id] }))).catch(() => {
310
+ throw new Error("That contract didn't answer like an NFT collection (ERC-721). Check the address and the token ids.");
311
+ });
312
+ const notMine = ids.filter((_, i) => String(owners[i]).toLowerCase() !== me);
313
+ if (notMine.length)
314
+ throw new Error(`This wallet doesn't own token${notMine.length > 1 ? "s" : ""} ${notMine.slice(0, 5).map(String).join(", ")}${notMine.length > 5 ? "…" : ""}.`);
315
+ let approvalTx;
316
+ if (!(await this.publicClient.readContract({ ...nft, functionName: "isApprovedForAll", args: [w.account.address, this.escrowFactory] }))) {
317
+ approvalTx = await w.writeContract({ ...nft, functionName: "setApprovalForAll", args: [this.escrowFactory, true], account: w.account, chain: w.chain });
318
+ await this.send(approvalTx);
319
+ }
320
+ const wlList = (o.wl?.list ?? []).map((a) => getAddress(a));
321
+ const now = Number((await this.publicClient.getBlock()).timestamp);
322
+ const gtdEnd = now + 90;
323
+ const commitEnd = gtdEnd + o.biddingSecs;
324
+ const tlockRound = drandRoundAt(commitEnd);
325
+ const revealEnd = Math.max(commitEnd + (o.revealSecs ?? DROP_DEFAULT_REVEAL_SECS), drandRoundTime(tlockRound) + 60);
326
+ const zero = `0x${"0".repeat(64)}`;
327
+ const params = {
328
+ creator: w.account.address,
329
+ supply: ids.length,
330
+ maxPerBid: o.maxPerBid ?? 1,
331
+ numTicks: o.numTicks ?? 200,
332
+ wlBoostBps: wlList.length ? boost : 0,
333
+ royaltyBps: 0,
334
+ gtdEnd: BigInt(gtdEnd),
335
+ commitEnd: BigInt(commitEnd),
336
+ revealEnd: BigInt(revealEnd),
337
+ tlockRound: BigInt(tlockRound),
338
+ floor: o.floor,
339
+ tickSize: o.tickSize ?? (o.floor / 20n || 1n),
340
+ gtdPrice: 0n,
341
+ gtdRoot: zero,
342
+ wlRoot: merkleTree(wlList.map(wlLeaf)).root,
221
343
  };
344
+ const links = cleanLinks(o.links);
345
+ const details = { v: 1, ...(o.image ? { image: o.image } : {}), ...(o.banner && imageOk(o.banner) ? { banner: o.banner } : {}), ...(links ? { links } : {}), ...(o.category?.trim() ? { category: o.category.trim().slice(0, 32) } : {}), ...(o.description ? { description: o.description.slice(0, 1000) } : {}), ...(wlList.length ? { wl: wlList } : {}) };
346
+ const detailsJson = JSON.stringify(details);
347
+ if (new TextEncoder().encode(detailsJson).length > DROP_MAX_DETAILS)
348
+ throw new Error("The images and lists are too big to publish together. Link the banner (https:// or ipfs://) instead of uploading it, or use a smaller image.");
349
+ const { result, request } = await this.publicClient.simulateContract({
350
+ address: this.escrowFactory,
351
+ abi: cloakEscrowDropFactoryAbi,
352
+ functionName: "createDrop",
353
+ args: [params, nft.address, ids, o.name.trim(), o.symbol.trim(), detailsJson],
354
+ account: w.account,
355
+ });
356
+ const txHash = await w.writeContract(request);
357
+ await this.send(txHash);
358
+ return { drop: result, txHash, approvalTx };
359
+ }
360
+ /**
361
+ * The token ids of `collection` a wallet holds, from the collection's Transfer events (works for any ERC-721, no
362
+ * enumeration needed). Capped at `max`.
363
+ */
364
+ async ownedTokens(collection, owner, opts = {}) {
365
+ const max = opts.max ?? ESCROW_MAX_TOKENS;
366
+ const c = { address: collection, abi: ERC721_ABI };
367
+ // ERC-721 Enumerable: read the wallet's tokens straight from the collection
368
+ const enumerable = await this.publicClient.readContract({ ...c, functionName: "supportsInterface", args: ["0x780e9d63"] }).catch(() => false);
369
+ if (enumerable) {
370
+ const n = Number(await this.publicClient.readContract({ ...c, functionName: "balanceOf", args: [owner] }));
371
+ const ids = (await this.reads(Array.from({ length: Math.min(n, max) }, (_, i) => ({ ...c, functionName: "tokenOfOwnerByIndex", args: [owner, BigInt(i)] }))));
372
+ return ids.sort((x, y) => (x < y ? -1 : 1));
373
+ }
374
+ // otherwise, the collection's Transfer events to the wallet, from the block it was deployed in
375
+ const ev = { type: "event", name: "Transfer", inputs: [{ type: "address", name: "from", indexed: true }, { type: "address", name: "to", indexed: true }, { type: "uint256", name: "tokenId", indexed: true }] };
376
+ const latest = await this.publicClient.getBlockNumber();
377
+ const from = opts.fromBlock ?? (await this.deployBlock(collection, latest));
378
+ const seen = new Set();
379
+ const get = (a, b) => this.publicClient.getLogs({ address: collection, event: ev, args: { to: owner }, fromBlock: a, toBlock: b });
380
+ try {
381
+ for (const l of await get(from, latest))
382
+ if (l.args.tokenId !== undefined)
383
+ seen.add(l.args.tokenId);
384
+ }
385
+ catch {
386
+ for (let a = from; a <= latest; a += 100000n) {
387
+ const b = a + 99999n > latest ? latest : a + 99999n;
388
+ for (const l of await get(a, b).catch(() => []))
389
+ if (l.args.tokenId !== undefined)
390
+ seen.add(l.args.tokenId);
391
+ }
392
+ }
393
+ const ids = [...seen].sort((a, b) => (a < b ? -1 : 1));
394
+ const owners = await this.reads(ids.map((id) => ({ ...c, functionName: "ownerOf", args: [id] }))).catch(() => ids.map(() => null));
395
+ return ids.filter((_, i) => String(owners[i]).toLowerCase() === owner.toLowerCase()).slice(0, max);
396
+ }
397
+ /** The block a contract was deployed in, by binary search on its code (0 if the node can't tell). */
398
+ async deployBlock(address, latest) {
399
+ try {
400
+ let lo = 0n;
401
+ let hi = latest;
402
+ if (!(await this.publicClient.getCode({ address, blockNumber: hi })))
403
+ return 0n;
404
+ while (lo < hi) {
405
+ const mid = (lo + hi) / 2n;
406
+ if (await this.publicClient.getCode({ address, blockNumber: mid }))
407
+ hi = mid;
408
+ else
409
+ lo = mid + 1n;
410
+ }
411
+ return lo;
412
+ }
413
+ catch {
414
+ return 0n;
415
+ }
222
416
  }
223
417
  /** The bid level for a price: the highest level at or below it (bids snap down, never up). */
224
418
  static tickFor(s, price) {
package/dist/index.d.ts CHANGED
@@ -7,3 +7,4 @@ export * from "./cloak.js";
7
7
  export * from "./drops.js";
8
8
  export * from "./private/index.js";
9
9
  export * from "./spots.js";
10
+ export * from "./names.js";
package/dist/index.js CHANGED
@@ -7,3 +7,4 @@ export * from "./cloak.js";
7
7
  export * from "./drops.js";
8
8
  export * from "./private/index.js";
9
9
  export * from "./spots.js";
10
+ export * from "./names.js";
@@ -0,0 +1,65 @@
1
+ import { type Address, type PublicClient } from "viem";
2
+ /**
3
+ * .ely names (Elysium Name Service): type `bona.ely` instead of a long address. Two read-only calls on one contract,
4
+ * no API key and no backend. Mainnet's address is published at launch; until then only testnet resolves.
5
+ */
6
+ export declare const ELY_NAMES: Record<number, Address>;
7
+ export declare const elyNamesAbi: readonly [{
8
+ readonly name: "resolve";
9
+ readonly type: "function";
10
+ readonly stateMutability: "view";
11
+ readonly inputs: readonly [{
12
+ readonly type: "string";
13
+ readonly name: "label";
14
+ }];
15
+ readonly outputs: readonly [{
16
+ readonly type: "address";
17
+ }];
18
+ }, {
19
+ readonly name: "primaryNameOf";
20
+ readonly type: "function";
21
+ readonly stateMutability: "view";
22
+ readonly inputs: readonly [{
23
+ readonly type: "address";
24
+ readonly name: "owner";
25
+ }];
26
+ readonly outputs: readonly [{
27
+ readonly type: "string";
28
+ }];
29
+ }, {
30
+ readonly name: "text";
31
+ readonly type: "function";
32
+ readonly stateMutability: "view";
33
+ readonly inputs: readonly [{
34
+ readonly type: "string";
35
+ readonly name: "label";
36
+ }, {
37
+ readonly type: "string";
38
+ readonly name: "key";
39
+ }];
40
+ readonly outputs: readonly [{
41
+ readonly type: "string";
42
+ }];
43
+ }];
44
+ /**
45
+ * The label of a .ely name, or null if the input isn't one: trimmed, lowercased, a trailing `.ely` removed, then
46
+ * 2 to 63 of a-z, 0-9 and hyphens, not starting or ending with a hyphen.
47
+ */
48
+ export declare function elyLabel(input: string): string | null;
49
+ /** True when the input looks like a .ely name (it ends in `.ely` and its label is valid). */
50
+ export declare const isElyName: (input: string) => boolean;
51
+ /**
52
+ * The wallet a .ely name points to now, or null if the name isn't registered, has expired, or the chain has no name
53
+ * service. Resolve at the moment of sending and don't store the result: names expire and change hands.
54
+ */
55
+ export declare function resolveEly(publicClient: PublicClient, name: string): Promise<Address | null>;
56
+ /** A wallet's chosen .ely name (like `bona.ely`), or null if it has none. Safe to display: only the owner can set it. */
57
+ export declare function elyNameOf(publicClient: PublicClient, address: Address): Promise<string | null>;
58
+ /**
59
+ * A recipient typed by a person: a 0x address as it is, or a .ely name resolved now. Throws a readable error for
60
+ * anything else, and for a name that doesn't resolve (never send to the zero address).
61
+ */
62
+ export declare function resolveRecipient(publicClient: PublicClient, input: string): Promise<{
63
+ address: Address;
64
+ name?: string;
65
+ }>;