@zoralabs/coins-sdk 0.2.4 → 0.2.6
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/CHANGELOG.md +12 -0
- package/dist/actions/createCoin.d.ts +1 -1
- package/dist/actions/createCoin.d.ts.map +1 -1
- package/dist/actions/tradeCoin.d.ts +52 -0
- package/dist/actions/tradeCoin.d.ts.map +1 -0
- package/dist/api/api-key.d.ts +2 -1
- package/dist/api/api-key.d.ts.map +1 -1
- package/dist/api/internal.d.ts +2 -2
- package/dist/api/internal.d.ts.map +1 -1
- package/dist/client/sdk.gen.d.ts +77 -1
- package/dist/client/sdk.gen.d.ts.map +1 -1
- package/dist/client/types.gen.d.ts +156 -0
- package/dist/client/types.gen.d.ts.map +1 -1
- package/dist/index.cjs +405 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +404 -0
- package/dist/index.js.map +1 -1
- package/dist/metadata/cleanAndValidateMetadataURI.d.ts +1 -1
- package/dist/metadata/cleanAndValidateMetadataURI.d.ts.map +1 -1
- package/dist/metadata/index.d.ts +1 -1
- package/dist/metadata/index.d.ts.map +1 -1
- package/dist/metadata/validateMetadataURIContent.d.ts +1 -1
- package/dist/metadata/validateMetadataURIContent.d.ts.map +1 -1
- package/dist/uploader/index.d.ts +10 -0
- package/dist/uploader/index.d.ts.map +1 -0
- package/dist/uploader/metadata.d.ts +44 -0
- package/dist/uploader/metadata.d.ts.map +1 -0
- package/dist/uploader/providers/zora.d.ts +18 -0
- package/dist/uploader/providers/zora.d.ts.map +1 -0
- package/dist/uploader/types.d.ts +21 -0
- package/dist/uploader/types.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/actions/createCoin.ts +2 -1
- package/src/actions/tradeCoin.ts +230 -0
- package/src/api/api-key.ts +5 -1
- package/src/api/internal.ts +3 -3
- package/src/client/sdk.gen.ts +26 -0
- package/src/client/types.gen.ts +161 -0
- package/src/index.ts +6 -0
- package/src/metadata/cleanAndValidateMetadataURI.ts +1 -5
- package/src/metadata/index.ts +1 -4
- package/src/metadata/validateMetadataURIContent.ts +2 -4
- package/src/uploader/index.ts +16 -0
- package/src/uploader/metadata.ts +214 -0
- package/src/uploader/providers/zora.ts +86 -0
- package/src/uploader/tests/metadata.test.ts +331 -0
- package/src/uploader/tests/providers.test.ts +131 -0
- package/src/uploader/types.ts +27 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateMetadataParameters,
|
|
3
|
+
Uploader,
|
|
4
|
+
UploadResult,
|
|
5
|
+
ValidMetadataURI,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
type Metadata = {
|
|
9
|
+
name: string;
|
|
10
|
+
symbol: string;
|
|
11
|
+
description: string;
|
|
12
|
+
image: string;
|
|
13
|
+
properties?: Record<string, string>;
|
|
14
|
+
animation_url?: string;
|
|
15
|
+
content?: {
|
|
16
|
+
uri: string;
|
|
17
|
+
mime: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function validateImageMimeType(mimeType: string) {
|
|
22
|
+
if (
|
|
23
|
+
![
|
|
24
|
+
"image/png",
|
|
25
|
+
"image/jpeg",
|
|
26
|
+
"image/jpg",
|
|
27
|
+
"image/gif",
|
|
28
|
+
"image/svg+xml",
|
|
29
|
+
].includes(mimeType)
|
|
30
|
+
) {
|
|
31
|
+
throw new Error("Image must be a PNG, JPEG, JPG, GIF or SVG");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getURLFromUploadResult(uploadResult: UploadResult) {
|
|
36
|
+
return new URL(uploadResult.url);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class CoinMetadataBuilder {
|
|
40
|
+
private name: string | undefined;
|
|
41
|
+
private description: string | undefined;
|
|
42
|
+
private symbol: string | undefined;
|
|
43
|
+
private imageFile: File | undefined;
|
|
44
|
+
private imageURL: URL | undefined;
|
|
45
|
+
private mediaFile: File | undefined;
|
|
46
|
+
private mediaURL: URL | undefined;
|
|
47
|
+
private mediaMimeType: string | undefined;
|
|
48
|
+
private properties: Record<string, string> | undefined;
|
|
49
|
+
|
|
50
|
+
withName(name: string) {
|
|
51
|
+
this.name = name;
|
|
52
|
+
if (typeof name !== "string") {
|
|
53
|
+
throw new Error("Name must be a string");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
withSymbol(symbol: string) {
|
|
60
|
+
this.symbol = symbol;
|
|
61
|
+
if (typeof symbol !== "string") {
|
|
62
|
+
throw new Error("Symbol must be a string");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
withDescription(description: string) {
|
|
69
|
+
this.description = description;
|
|
70
|
+
if (typeof description !== "string") {
|
|
71
|
+
throw new Error("Description must be a string");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
withImage(image: File) {
|
|
78
|
+
if (this.imageURL) {
|
|
79
|
+
throw new Error("Image URL already set");
|
|
80
|
+
}
|
|
81
|
+
if (!(image instanceof File)) {
|
|
82
|
+
throw new Error("Image must be a File");
|
|
83
|
+
}
|
|
84
|
+
validateImageMimeType(image.type);
|
|
85
|
+
this.imageFile = image;
|
|
86
|
+
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
withImageURI(imageURI: string) {
|
|
91
|
+
if (this.imageFile) {
|
|
92
|
+
throw new Error("Image file already set");
|
|
93
|
+
}
|
|
94
|
+
if (typeof imageURI !== "string") {
|
|
95
|
+
throw new Error("Image URI must be a string");
|
|
96
|
+
}
|
|
97
|
+
const url = new URL(imageURI);
|
|
98
|
+
this.imageURL = url;
|
|
99
|
+
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
withProperties(properties: Record<string, string>) {
|
|
104
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
105
|
+
if (typeof key !== "string") {
|
|
106
|
+
throw new Error("Property key must be a string");
|
|
107
|
+
}
|
|
108
|
+
if (typeof value !== "string") {
|
|
109
|
+
throw new Error("Property value must be a string");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!this.properties) {
|
|
113
|
+
this.properties = {};
|
|
114
|
+
}
|
|
115
|
+
this.properties = { ...this.properties, ...properties };
|
|
116
|
+
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
withMedia(media: File) {
|
|
121
|
+
if (this.mediaURL) {
|
|
122
|
+
throw new Error("Media URL already set");
|
|
123
|
+
}
|
|
124
|
+
if (!(media instanceof File)) {
|
|
125
|
+
throw new Error("Media must be a File");
|
|
126
|
+
}
|
|
127
|
+
this.mediaMimeType = media.type;
|
|
128
|
+
this.mediaFile = media;
|
|
129
|
+
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
withMediaURI(mediaURI: string, mediaMimeType: string | undefined) {
|
|
134
|
+
if (this.mediaFile) {
|
|
135
|
+
throw new Error("Media file already set");
|
|
136
|
+
}
|
|
137
|
+
if (typeof mediaURI !== "string") {
|
|
138
|
+
throw new Error("Media URI must be a string");
|
|
139
|
+
}
|
|
140
|
+
const url = new URL(mediaURI);
|
|
141
|
+
this.mediaURL = url;
|
|
142
|
+
this.mediaMimeType = mediaMimeType;
|
|
143
|
+
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
validate() {
|
|
148
|
+
if (!this.name) {
|
|
149
|
+
throw new Error("Name is required");
|
|
150
|
+
}
|
|
151
|
+
if (!this.symbol) {
|
|
152
|
+
throw new Error("Symbol is required");
|
|
153
|
+
}
|
|
154
|
+
if (!this.imageFile && !this.imageURL) {
|
|
155
|
+
throw new Error("Image is required");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
generateMetadata(): Metadata {
|
|
162
|
+
return {
|
|
163
|
+
name: this.name!,
|
|
164
|
+
symbol: this.symbol!,
|
|
165
|
+
description: this.description!,
|
|
166
|
+
image: this.imageURL!.toString(),
|
|
167
|
+
animation_url: this.mediaURL?.toString(),
|
|
168
|
+
content: this.mediaURL
|
|
169
|
+
? {
|
|
170
|
+
uri: this.mediaURL?.toString(),
|
|
171
|
+
mime: this.mediaMimeType,
|
|
172
|
+
}
|
|
173
|
+
: undefined,
|
|
174
|
+
properties: this.properties,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async upload(uploader: Uploader): Promise<{
|
|
179
|
+
url: ValidMetadataURI;
|
|
180
|
+
createMetadataParameters: CreateMetadataParameters;
|
|
181
|
+
metadata: Metadata;
|
|
182
|
+
}> {
|
|
183
|
+
this.validate();
|
|
184
|
+
|
|
185
|
+
if (this.imageFile) {
|
|
186
|
+
const uploadResult = await uploader.upload(this.imageFile);
|
|
187
|
+
this.imageURL = getURLFromUploadResult(uploadResult);
|
|
188
|
+
}
|
|
189
|
+
if (this.mediaFile) {
|
|
190
|
+
const uploadResult = await uploader.upload(this.mediaFile);
|
|
191
|
+
this.mediaURL = getURLFromUploadResult(uploadResult);
|
|
192
|
+
}
|
|
193
|
+
const metadata = this.generateMetadata();
|
|
194
|
+
const uploadResult = await uploader.upload(
|
|
195
|
+
new File([JSON.stringify(metadata)], "metadata.json", {
|
|
196
|
+
type: "application/json",
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
url: getURLFromUploadResult(uploadResult).toString() as ValidMetadataURI,
|
|
202
|
+
createMetadataParameters: {
|
|
203
|
+
name: this.name!,
|
|
204
|
+
symbol: this.symbol!,
|
|
205
|
+
uri: uploadResult.url as `ipfs://${string}`,
|
|
206
|
+
},
|
|
207
|
+
metadata,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function createMetadataBuilder() {
|
|
213
|
+
return new CoinMetadataBuilder();
|
|
214
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Address } from "viem";
|
|
2
|
+
import { Uploader, UploadResult } from "../types";
|
|
3
|
+
import { getApiKey } from "../../api/api-key";
|
|
4
|
+
import { setCreateUploadJwt } from "../../api/internal";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Zora IPFS uploader implementation
|
|
8
|
+
*/
|
|
9
|
+
export class ZoraUploader implements Uploader {
|
|
10
|
+
constructor(creatorAddress: Address) {
|
|
11
|
+
this.creatorAddress = creatorAddress;
|
|
12
|
+
if (!getApiKey()) {
|
|
13
|
+
throw new Error("API key is required for metadata interactions");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private creatorAddress: Address;
|
|
18
|
+
private jwtApiKey: string | undefined;
|
|
19
|
+
private jwtApiKeyExpiresAt: number | undefined;
|
|
20
|
+
|
|
21
|
+
async getJWTApiKey() {
|
|
22
|
+
if (
|
|
23
|
+
this.jwtApiKey &&
|
|
24
|
+
this.jwtApiKeyExpiresAt &&
|
|
25
|
+
this.jwtApiKeyExpiresAt > Date.now()
|
|
26
|
+
) {
|
|
27
|
+
return this.jwtApiKey;
|
|
28
|
+
}
|
|
29
|
+
// Expires in 1 hour
|
|
30
|
+
this.jwtApiKeyExpiresAt = Date.now() + 1000 * 60 * 60;
|
|
31
|
+
|
|
32
|
+
const response = await setCreateUploadJwt({
|
|
33
|
+
creatorAddress: this.creatorAddress,
|
|
34
|
+
});
|
|
35
|
+
this.jwtApiKey = response.data?.createUploadJwtFromApiKey;
|
|
36
|
+
if (!this.jwtApiKey) {
|
|
37
|
+
throw new Error("Failed to create upload JWT");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return this.jwtApiKey;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async upload(file: File): Promise<UploadResult> {
|
|
44
|
+
const jwtApiKey = await this.getJWTApiKey();
|
|
45
|
+
const formData = new FormData();
|
|
46
|
+
formData.append("file", file, file.name);
|
|
47
|
+
|
|
48
|
+
const response = await fetch(
|
|
49
|
+
"https://ipfs-uploader.zora.co/api/v0/add?cid-version=1",
|
|
50
|
+
{
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${jwtApiKey}`,
|
|
54
|
+
Accept: "*/*",
|
|
55
|
+
},
|
|
56
|
+
body: formData,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
console.error(await response.text());
|
|
62
|
+
throw new Error(`Failed to upload file: ${response.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = (await response.json()) as {
|
|
66
|
+
cid: string;
|
|
67
|
+
size: number | undefined;
|
|
68
|
+
mimeType: string | undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
url: `ipfs://${data.cid}`,
|
|
73
|
+
size: data.size,
|
|
74
|
+
mimeType: data.mimeType,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a new Zora IPFS uploader
|
|
81
|
+
*/
|
|
82
|
+
export function createZoraUploaderForCreator(
|
|
83
|
+
creatorAddress: Address,
|
|
84
|
+
): Uploader {
|
|
85
|
+
return new ZoraUploader(creatorAddress);
|
|
86
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
CoinMetadataBuilder,
|
|
4
|
+
validateImageMimeType,
|
|
5
|
+
getURLFromUploadResult,
|
|
6
|
+
} from "../metadata";
|
|
7
|
+
import { Uploader, UploadResult } from "../types";
|
|
8
|
+
|
|
9
|
+
enum UploadResultType {
|
|
10
|
+
Image = "image",
|
|
11
|
+
Media = "media",
|
|
12
|
+
Metadata = "metadata",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Create a mock implementation of the Uploader interface
|
|
16
|
+
const createMockUploader = (resultList: UploadResultType[]) => {
|
|
17
|
+
// Pre-defined results to return from the upload method
|
|
18
|
+
const mockImageResult: UploadResult = {
|
|
19
|
+
url: "ipfs://image-cid",
|
|
20
|
+
size: 100,
|
|
21
|
+
mimeType: "image/png",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const mockMediaResult: UploadResult = {
|
|
25
|
+
url: "ipfs://media-cid",
|
|
26
|
+
size: 200,
|
|
27
|
+
mimeType: "video/mp4",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mockMetadataResult: UploadResult = {
|
|
31
|
+
url: "ipfs://metadata-cid",
|
|
32
|
+
size: 300,
|
|
33
|
+
mimeType: "application/json",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const uploadedFiles: File[] = [];
|
|
37
|
+
let callCount = 0;
|
|
38
|
+
|
|
39
|
+
const mockUploader: Uploader = {
|
|
40
|
+
upload: vi.fn(async (file: File): Promise<UploadResult> => {
|
|
41
|
+
uploadedFiles.push(file);
|
|
42
|
+
callCount++;
|
|
43
|
+
|
|
44
|
+
switch (resultList[callCount - 1]) {
|
|
45
|
+
case UploadResultType.Image:
|
|
46
|
+
return mockImageResult;
|
|
47
|
+
case UploadResultType.Media:
|
|
48
|
+
return mockMediaResult;
|
|
49
|
+
case UploadResultType.Metadata:
|
|
50
|
+
return mockMetadataResult;
|
|
51
|
+
default:
|
|
52
|
+
throw new Error(`Invalid result type: ${resultList[callCount - 1]}`);
|
|
53
|
+
}
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
uploader: mockUploader,
|
|
59
|
+
uploadedFiles,
|
|
60
|
+
mockImageResult,
|
|
61
|
+
mockMediaResult,
|
|
62
|
+
mockMetadataResult,
|
|
63
|
+
reset: () => {
|
|
64
|
+
callCount = 0;
|
|
65
|
+
uploadedFiles.length = 0;
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe("validateImageMimeType", () => {
|
|
72
|
+
it("should not throw for valid image types", () => {
|
|
73
|
+
const validTypes = [
|
|
74
|
+
"image/png",
|
|
75
|
+
"image/jpeg",
|
|
76
|
+
"image/jpg",
|
|
77
|
+
"image/gif",
|
|
78
|
+
"image/svg+xml",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
validTypes.forEach((type) => {
|
|
82
|
+
expect(() => validateImageMimeType(type)).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should throw for invalid image types", () => {
|
|
87
|
+
const invalidTypes = ["image/webp", "application/pdf", "text/plain"];
|
|
88
|
+
|
|
89
|
+
invalidTypes.forEach((type) => {
|
|
90
|
+
expect(() => validateImageMimeType(type)).toThrow(
|
|
91
|
+
"Image must be a PNG, JPEG, JPG, GIF or SVG",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("getURLFromUploadResult", () => {
|
|
98
|
+
it("should convert an upload result URL to a URL object", () => {
|
|
99
|
+
const result: UploadResult = {
|
|
100
|
+
url: "ipfs://bafybeiguslukdujd22p7ix53rcszgbg4ine464g33zk2st3lnjpx4uvmri",
|
|
101
|
+
size: 100,
|
|
102
|
+
mimeType: "image/png",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const url = getURLFromUploadResult(result);
|
|
106
|
+
expect(url).toBeInstanceOf(URL);
|
|
107
|
+
expect(url.toString()).toBe(
|
|
108
|
+
"ipfs://bafybeiguslukdujd22p7ix53rcszgbg4ine464g33zk2st3lnjpx4uvmri",
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should throw for invalid URLs", () => {
|
|
113
|
+
const result: UploadResult = {
|
|
114
|
+
url: "invalid-url",
|
|
115
|
+
size: 100,
|
|
116
|
+
mimeType: "image/png",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
expect(() => getURLFromUploadResult(result)).toThrow();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("CoinMetadataBuilder", () => {
|
|
124
|
+
let mockUploaderData: ReturnType<typeof createMockUploader>;
|
|
125
|
+
let builder: CoinMetadataBuilder;
|
|
126
|
+
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
mockUploaderData = createMockUploader([
|
|
129
|
+
UploadResultType.Image,
|
|
130
|
+
UploadResultType.Media,
|
|
131
|
+
UploadResultType.Metadata,
|
|
132
|
+
]);
|
|
133
|
+
builder = new CoinMetadataBuilder();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("builder methods", () => {
|
|
137
|
+
it("should set name and return self", () => {
|
|
138
|
+
const result = builder.withName("Test Coin");
|
|
139
|
+
expect(result).toBe(builder);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should set symbol and return self", () => {
|
|
143
|
+
const result = builder.withSymbol("TEST");
|
|
144
|
+
expect(result).toBe(builder);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should set description and return self", () => {
|
|
148
|
+
const result = builder.withDescription("Test Description");
|
|
149
|
+
expect(result).toBe(builder);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should set image file and return self", () => {
|
|
153
|
+
const imageFile = new File(["test"], "test.png", { type: "image/png" });
|
|
154
|
+
const result = builder.withImage(imageFile);
|
|
155
|
+
expect(result).toBe(builder);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should throw if image URL is already set when setting image file", () => {
|
|
159
|
+
builder.withImageURI("ipfs://test");
|
|
160
|
+
const imageFile = new File(["test"], "test.png", { type: "image/png" });
|
|
161
|
+
expect(() => builder.withImage(imageFile)).toThrow(
|
|
162
|
+
"Image URL already set",
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should throw if image file is already set when setting image URI", () => {
|
|
167
|
+
const imageFile = new File(["test"], "test.png", { type: "image/png" });
|
|
168
|
+
builder.withImage(imageFile);
|
|
169
|
+
expect(() => builder.withImageURI("ipfs://test")).toThrow(
|
|
170
|
+
"Image file already set",
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should set media file and return self", () => {
|
|
175
|
+
const mediaFile = new File(["test"], "test.mp4", { type: "video/mp4" });
|
|
176
|
+
const result = builder.withMedia(mediaFile);
|
|
177
|
+
expect(result).toBe(builder);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should throw if media URL is already set when setting media file", () => {
|
|
181
|
+
builder.withMediaURI("ipfs://test", "video/mp4");
|
|
182
|
+
const mediaFile = new File(["test"], "test.mp4", { type: "video/mp4" });
|
|
183
|
+
expect(() => builder.withMedia(mediaFile)).toThrow(
|
|
184
|
+
"Media URL already set",
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should throw if media file is already set when setting media URI", () => {
|
|
189
|
+
const mediaFile = new File(["test"], "test.mp4", { type: "video/mp4" });
|
|
190
|
+
builder.withMedia(mediaFile);
|
|
191
|
+
expect(() => builder.withMediaURI("ipfs://test", "video/mp4")).toThrow(
|
|
192
|
+
"Media file already set",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should set properties and return self", () => {
|
|
197
|
+
const properties = { key1: "value1", key2: "value2" };
|
|
198
|
+
const result = builder.withProperties(properties);
|
|
199
|
+
expect(result).toBe(builder);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should throw if property value is not a string", () => {
|
|
203
|
+
const properties = { key1: "value1", key2: 123 as any };
|
|
204
|
+
expect(() => builder.withProperties(properties)).toThrow(
|
|
205
|
+
"Property value must be a string",
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("validate", () => {
|
|
211
|
+
it("should throw if name is not set", () => {
|
|
212
|
+
builder
|
|
213
|
+
.withSymbol("TEST")
|
|
214
|
+
.withImage(new File(["test"], "test.png", { type: "image/png" }));
|
|
215
|
+
expect(() => builder.validate()).toThrow("Name is required");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should throw if symbol is not set", () => {
|
|
219
|
+
builder
|
|
220
|
+
.withName("Test Coin")
|
|
221
|
+
.withImage(new File(["test"], "test.png", { type: "image/png" }));
|
|
222
|
+
expect(() => builder.validate()).toThrow("Symbol is required");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should throw if image is not set", () => {
|
|
226
|
+
builder.withName("Test Coin").withSymbol("TEST");
|
|
227
|
+
expect(() => builder.validate()).toThrow("Image is required");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should not throw if all required fields are set", () => {
|
|
231
|
+
builder
|
|
232
|
+
.withName("Test Coin")
|
|
233
|
+
.withSymbol("TEST")
|
|
234
|
+
.withImage(new File(["test"], "test.png", { type: "image/png" }));
|
|
235
|
+
|
|
236
|
+
expect(() => builder.validate()).not.toThrow();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("generateMetadata", () => {
|
|
241
|
+
it("should generate correct metadata", () => {
|
|
242
|
+
builder
|
|
243
|
+
.withName("Test Coin")
|
|
244
|
+
.withSymbol("TEST")
|
|
245
|
+
.withDescription("Test Description")
|
|
246
|
+
.withImageURI("ipfs://image-cid")
|
|
247
|
+
.withMediaURI("ipfs://media-cid", "video/mp4")
|
|
248
|
+
.withProperties({ key1: "value1", key2: "value2" });
|
|
249
|
+
|
|
250
|
+
const metadata = builder.generateMetadata();
|
|
251
|
+
|
|
252
|
+
expect(metadata).toEqual({
|
|
253
|
+
name: "Test Coin",
|
|
254
|
+
symbol: "TEST",
|
|
255
|
+
description: "Test Description",
|
|
256
|
+
image: "ipfs://image-cid",
|
|
257
|
+
animation_url: "ipfs://media-cid",
|
|
258
|
+
content: {
|
|
259
|
+
uri: "ipfs://media-cid",
|
|
260
|
+
mime: "video/mp4",
|
|
261
|
+
},
|
|
262
|
+
properties: { key1: "value1", key2: "value2" },
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("upload", () => {
|
|
268
|
+
it("should upload image, media and metadata", async () => {
|
|
269
|
+
const imageFile = new File(["test-image"], "test.png", {
|
|
270
|
+
type: "image/png",
|
|
271
|
+
});
|
|
272
|
+
const mediaFile = new File(["test-media"], "test.mp4", {
|
|
273
|
+
type: "video/mp4",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
builder
|
|
277
|
+
.withName("Test Coin")
|
|
278
|
+
.withSymbol("TEST")
|
|
279
|
+
.withDescription("Test Description")
|
|
280
|
+
.withImage(imageFile)
|
|
281
|
+
.withMedia(mediaFile)
|
|
282
|
+
.withProperties({ key1: "value1", key2: "value2" });
|
|
283
|
+
|
|
284
|
+
const result = await builder.upload(mockUploaderData.uploader);
|
|
285
|
+
|
|
286
|
+
// Verify uploader was called 3 times
|
|
287
|
+
expect(mockUploaderData.uploader.upload).toHaveBeenCalledTimes(3);
|
|
288
|
+
|
|
289
|
+
// Verify files were uploaded in the right order
|
|
290
|
+
expect(mockUploaderData.uploadedFiles[0]).toBe(imageFile);
|
|
291
|
+
expect(mockUploaderData.uploadedFiles[1]).toBe(mediaFile);
|
|
292
|
+
expect(mockUploaderData.uploadedFiles[2]?.type).toBe("application/json");
|
|
293
|
+
|
|
294
|
+
// Verify result
|
|
295
|
+
expect(result.url.toString()).toBe("ipfs://metadata-cid");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should use existing image and media URLs if provided", async () => {
|
|
299
|
+
mockUploaderData = createMockUploader([UploadResultType.Metadata]);
|
|
300
|
+
builder = new CoinMetadataBuilder();
|
|
301
|
+
|
|
302
|
+
builder
|
|
303
|
+
.withName("Test Coin")
|
|
304
|
+
.withSymbol("TEST")
|
|
305
|
+
.withDescription("Test Description")
|
|
306
|
+
.withImageURI("ipfs://existing-image")
|
|
307
|
+
.withMediaURI("ipfs://existing-media", "video/mp4")
|
|
308
|
+
.withProperties({ key1: "value1", key2: "value2" });
|
|
309
|
+
|
|
310
|
+
const result = await builder.upload(mockUploaderData.uploader);
|
|
311
|
+
|
|
312
|
+
// Should only upload metadata (1 upload)
|
|
313
|
+
expect(mockUploaderData.uploader.upload).toHaveBeenCalledTimes(1);
|
|
314
|
+
|
|
315
|
+
// The upload should be the metadata
|
|
316
|
+
expect(mockUploaderData.uploadedFiles[0]?.type).toBe("application/json");
|
|
317
|
+
|
|
318
|
+
// Verify result
|
|
319
|
+
expect(result.url.toString()).toBe("ipfs://metadata-cid");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should validate before uploading", async () => {
|
|
323
|
+
// Missing required fields
|
|
324
|
+
builder.withName("Test Coin");
|
|
325
|
+
|
|
326
|
+
await expect(builder.upload(mockUploaderData.uploader)).rejects.toThrow(
|
|
327
|
+
"Symbol is required",
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|