@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.
- package/dist/abis.d.ts +1599 -0
- package/dist/abis.js +2061 -0
- package/dist/bytecode.d.ts +4 -2
- package/dist/bytecode.js +1 -1
- package/dist/deployments.d.ts +2 -0
- package/dist/deployments.js +5 -3
- package/dist/drops.d.ts +182 -1
- package/dist/drops.js +203 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/names.d.ts +65 -0
- package/dist/names.js +59 -0
- package/dist/private/prover.d.ts +10 -3
- package/dist/private/prover.js +38 -29
- package/package.json +1 -1
package/dist/deployments.d.ts
CHANGED
|
@@ -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). */
|
package/dist/deployments.js
CHANGED
|
@@ -25,18 +25,20 @@ export const deployments = {
|
|
|
25
25
|
shared: "0x0ce2ecb0195e07899377e3d7b01e927ab3f87610",
|
|
26
26
|
sharedBlock: 21426599n,
|
|
27
27
|
sharedRouter: "0xe2a0769a5acbd987c1b6725247c56187a112dc7e",
|
|
28
|
-
sharedBatcher: "
|
|
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
|
-
|
|
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://
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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(),
|
|
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.
|
|
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
package/dist/index.js
CHANGED
package/dist/names.d.ts
ADDED
|
@@ -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
|
+
}>;
|