@nina-protocol/nina-db 0.0.106 → 0.0.107
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/knexfile.js +4 -0
- package/dist/migrations/20250714000001_release_publickey_mint_nullable.js +18 -0
- package/dist/migrations/20250723000000_add_releases_solana_address.js +20 -0
- package/dist/models/Release.js +110 -87
- package/dist/models/Transaction.js +1 -0
- package/package.json +1 -1
- package/src/knexfile.js +5 -0
- package/src/migrations/20250714000001_release_publickey_mint_nullable.js +19 -0
- package/src/migrations/20250723000000_add_releases_solana_address.js +21 -0
- package/src/models/Release.js +165 -114
- package/src/models/Transaction.js +1 -0
package/dist/knexfile.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* @type { Object.<string, import("knex").Knex.Config> }
|
|
3
3
|
*/
|
|
4
4
|
import "dotenv/config.js";
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
5
9
|
export default {
|
|
6
10
|
development: {
|
|
7
11
|
client: 'postgresql',
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param { import("knex").Knex } knex
|
|
3
|
+
* @returns { Promise<void> }
|
|
4
|
+
*/
|
|
5
|
+
export async function up(knex) {
|
|
6
|
+
await knex.schema.alterTable('releases', (table) => {
|
|
7
|
+
table.string('mint').nullable().alter();
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* @param { import("knex").Knex } knex
|
|
12
|
+
* @returns { Promise<void> }
|
|
13
|
+
*/
|
|
14
|
+
export async function down(knex) {
|
|
15
|
+
await knex.schema.alterTable('releases', (table) => {
|
|
16
|
+
table.string('mint').notNullable().alter();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param { import("knex").Knex } knex
|
|
3
|
+
* @returns { Promise<void> }
|
|
4
|
+
*/
|
|
5
|
+
export const up = function (knex) {
|
|
6
|
+
return knex.schema.table('releases', table => {
|
|
7
|
+
table.string('solanaAddress').nullable();
|
|
8
|
+
}).then(() => {
|
|
9
|
+
return knex('releases').whereNotNull('mint').update('solanaAddress', knex.ref('publicKey'));
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* @param { import("knex").Knex } knex
|
|
14
|
+
* @returns { Promise<void> }
|
|
15
|
+
*/
|
|
16
|
+
export const down = function (knex) {
|
|
17
|
+
return knex.schema.table('releases', table => {
|
|
18
|
+
table.dropColumn('solanaAddress');
|
|
19
|
+
});
|
|
20
|
+
};
|
package/dist/models/Release.js
CHANGED
|
@@ -13,39 +13,39 @@ import promiseRetry from 'promise-retry';
|
|
|
13
13
|
import { customAlphabet } from 'nanoid';
|
|
14
14
|
import { getTokenMetadata } from '@solana/spl-token';
|
|
15
15
|
const ensureHttps = (uri) => {
|
|
16
|
-
if (!uri.startsWith(
|
|
16
|
+
if (!uri.startsWith("http://") && !uri.startsWith("https://")) {
|
|
17
17
|
return `https://${uri}`;
|
|
18
18
|
}
|
|
19
19
|
return uri;
|
|
20
20
|
};
|
|
21
|
-
const alphabet =
|
|
21
|
+
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
22
22
|
const randomStringGenerator = customAlphabet(alphabet, 12);
|
|
23
23
|
export default class Release extends Model {
|
|
24
|
-
static tableName =
|
|
25
|
-
static idColumn =
|
|
24
|
+
static tableName = "releases";
|
|
25
|
+
static idColumn = "id";
|
|
26
26
|
static jsonSchema = {
|
|
27
|
-
type:
|
|
28
|
-
required: [
|
|
27
|
+
type: "object",
|
|
28
|
+
required: ["metadata", "datetime", "slug", "price"],
|
|
29
29
|
properties: {
|
|
30
|
-
publicKey: { type:
|
|
31
|
-
mint: { type:
|
|
32
|
-
slug: { type:
|
|
30
|
+
publicKey: { type: ["string"] },
|
|
31
|
+
mint: { type: "string" },
|
|
32
|
+
slug: { type: "string" },
|
|
33
33
|
metadata: {
|
|
34
|
-
type:
|
|
35
|
-
required: [
|
|
34
|
+
type: "object",
|
|
35
|
+
required: ["name", "symbol", "description", "image", "properties"],
|
|
36
36
|
properties: {
|
|
37
|
-
name: { type:
|
|
38
|
-
symbol: { type:
|
|
39
|
-
description: { type:
|
|
37
|
+
name: { type: "string" },
|
|
38
|
+
symbol: { type: "string" },
|
|
39
|
+
description: { type: "string" },
|
|
40
40
|
properties: {
|
|
41
|
-
type:
|
|
41
|
+
type: "object",
|
|
42
42
|
properties: {
|
|
43
|
-
artist: { type:
|
|
44
|
-
title: { type:
|
|
45
|
-
date: { type:
|
|
46
|
-
files: { type:
|
|
47
|
-
category: { type:
|
|
48
|
-
creators: { type:
|
|
43
|
+
artist: { type: "string" },
|
|
44
|
+
title: { type: "string" },
|
|
45
|
+
date: { type: "string" },
|
|
46
|
+
files: { type: "array" },
|
|
47
|
+
category: { type: "string" },
|
|
48
|
+
creators: { type: "array" }
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -76,13 +76,13 @@ export default class Release extends Model {
|
|
|
76
76
|
return result;
|
|
77
77
|
}
|
|
78
78
|
catch (error) {
|
|
79
|
-
console.log(
|
|
79
|
+
console.log("error fetching release account", error);
|
|
80
80
|
retry(error);
|
|
81
81
|
}
|
|
82
82
|
}, {
|
|
83
83
|
retries: 50,
|
|
84
84
|
minTimeout: 500,
|
|
85
|
-
maxTimeout: 1500
|
|
85
|
+
maxTimeout: 1500
|
|
86
86
|
});
|
|
87
87
|
let metadataAccount;
|
|
88
88
|
if (programId === process.env.NINA_PROGRAM_V2_ID) {
|
|
@@ -92,14 +92,16 @@ export default class Release extends Model {
|
|
|
92
92
|
metadataAccount = (await metaplex.nfts().findAllByMintList({ mints: [releaseAccount.releaseMint] }, { commitment: 'confirmed' }))[0];
|
|
93
93
|
}
|
|
94
94
|
if (!metadataAccount) {
|
|
95
|
-
throw new Error(
|
|
95
|
+
throw new Error("No metadata account found for release - is not a complete release");
|
|
96
96
|
}
|
|
97
97
|
let json;
|
|
98
98
|
try {
|
|
99
|
-
json = (await axios.get(ensureHttps(metadataAccount.uri
|
|
99
|
+
json = (await axios.get(ensureHttps(metadataAccount.uri
|
|
100
|
+
.replace("www.", "")
|
|
101
|
+
.replace("arweave.net", "gateway.irys.xyz")))).data;
|
|
100
102
|
}
|
|
101
103
|
catch (error) {
|
|
102
|
-
json = (await axios.get(ensureHttps(metadataAccount.uri.replace(
|
|
104
|
+
json = (await axios.get(ensureHttps(metadataAccount.uri.replace("gateway.irys.xyz", "arweave.net")))).data;
|
|
103
105
|
}
|
|
104
106
|
const slug = await this.generateSlug(json);
|
|
105
107
|
let publisher = await Account.findOrCreate(releaseAccount.authority.toBase58());
|
|
@@ -116,21 +118,27 @@ export default class Release extends Model {
|
|
|
116
118
|
if (hubPublicKey) {
|
|
117
119
|
const hub = await Hub.query().findOne({ publicKey: hubPublicKey });
|
|
118
120
|
await release.$query().patch({ hubId: hub.id });
|
|
119
|
-
await Hub.relatedQuery(
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
await Hub.relatedQuery("releases")
|
|
122
|
+
.for(hub.id)
|
|
123
|
+
.patch({
|
|
124
|
+
visible: true
|
|
125
|
+
})
|
|
126
|
+
.where({ id: release.id });
|
|
122
127
|
}
|
|
123
128
|
return release;
|
|
124
129
|
}
|
|
125
130
|
catch (error) {
|
|
126
|
-
console.log(
|
|
131
|
+
console.log("error finding or creating release: ", error);
|
|
127
132
|
return null;
|
|
128
133
|
}
|
|
129
134
|
};
|
|
130
135
|
static createRelease = async ({ publicKey, mint, metadata, datetime, publisherId, releaseAccount, programId }) => {
|
|
131
136
|
const slug = await this.generateSlug(metadata);
|
|
132
|
-
const price = releaseAccount.account?.price?.toNumber() ||
|
|
133
|
-
|
|
137
|
+
const price = releaseAccount.account?.price?.toNumber() ||
|
|
138
|
+
releaseAccount?.price?.toNumber() ||
|
|
139
|
+
0;
|
|
140
|
+
const paymentMint = releaseAccount.account?.paymentMint.toBase58() ||
|
|
141
|
+
releaseAccount?.paymentMint.toBase58();
|
|
134
142
|
const release = await Release.query().insertGraph({
|
|
135
143
|
publicKey,
|
|
136
144
|
mint,
|
|
@@ -146,7 +154,11 @@ export default class Release extends Model {
|
|
|
146
154
|
if (metadata.properties.tags) {
|
|
147
155
|
for await (let tag of metadata.properties.tags) {
|
|
148
156
|
const tagRecord = await Tag.findOrCreate(tag);
|
|
149
|
-
await Release.relatedQuery(
|
|
157
|
+
await Release.relatedQuery("tags")
|
|
158
|
+
.for(release.id)
|
|
159
|
+
.relate(tagRecord.id)
|
|
160
|
+
.onConflict(["tagId", "releaseId"])
|
|
161
|
+
.ignore();
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
164
|
if (programId === process.env.NINA_PROGRAM_ID) {
|
|
@@ -159,19 +171,27 @@ export default class Release extends Model {
|
|
|
159
171
|
const royaltyRecipients = releaseData.account?.royaltyRecipients || releaseData.royaltyRecipients;
|
|
160
172
|
for await (let recipient of royaltyRecipients) {
|
|
161
173
|
try {
|
|
162
|
-
if (recipient.recipientAuthority.toBase58() !==
|
|
174
|
+
if (recipient.recipientAuthority.toBase58() !==
|
|
175
|
+
"11111111111111111111111111111111") {
|
|
163
176
|
const recipientAccount = await Account.findOrCreate(recipient.recipientAuthority.toBase58());
|
|
164
|
-
const revenueShares = (await recipientAccount.$relatedQuery(
|
|
165
|
-
if (!revenueShares.includes(releaseRecord.id) &&
|
|
166
|
-
|
|
177
|
+
const revenueShares = (await recipientAccount.$relatedQuery("revenueShares")).map((revenueShare) => revenueShare.id);
|
|
178
|
+
if (!revenueShares.includes(releaseRecord.id) &&
|
|
179
|
+
recipient.percentShare.toNumber() > 0) {
|
|
180
|
+
await Account.relatedQuery("revenueShares")
|
|
181
|
+
.for(recipientAccount.id)
|
|
182
|
+
.relate(releaseRecord.id);
|
|
167
183
|
}
|
|
168
|
-
else if (revenueShares.includes(releaseRecord.id) &&
|
|
169
|
-
|
|
184
|
+
else if (revenueShares.includes(releaseRecord.id) &&
|
|
185
|
+
recipient.percentShare.toNumber() === 0) {
|
|
186
|
+
await Account.relatedQuery("revenueShares")
|
|
187
|
+
.for(recipientAccount.id)
|
|
188
|
+
.unrelate()
|
|
189
|
+
.where("id", releaseRecord.id);
|
|
170
190
|
}
|
|
171
191
|
}
|
|
172
192
|
}
|
|
173
193
|
catch (error) {
|
|
174
|
-
console.log(
|
|
194
|
+
console.log("error processing royaltyRecipients: ", error);
|
|
175
195
|
}
|
|
176
196
|
}
|
|
177
197
|
};
|
|
@@ -181,14 +201,15 @@ export default class Release extends Model {
|
|
|
181
201
|
string = string.substring(0, 200);
|
|
182
202
|
}
|
|
183
203
|
const slug = string
|
|
184
|
-
.normalize(
|
|
204
|
+
.normalize("NFKD")
|
|
205
|
+
.replace(/[\u0300-\u036F]/g, "") // remove accents and convert to closest ascii equivalent
|
|
185
206
|
.toLowerCase() // convert to lowercase
|
|
186
|
-
.replace(
|
|
187
|
-
.replace(/ +/g,
|
|
188
|
-
.replace(/ /g,
|
|
189
|
-
.replace(/[^a-zA-Z0-9-]/g,
|
|
190
|
-
.replace(/-+/g,
|
|
191
|
-
.replace(/-$/,
|
|
207
|
+
.replace("-", "") // remove hyphens
|
|
208
|
+
.replace(/ +/g, " ") // remove spaces
|
|
209
|
+
.replace(/ /g, "-") // replace spaces with hyphens
|
|
210
|
+
.replace(/[^a-zA-Z0-9-]/g, "-") // replace non-alphanumeric characters with hyphens
|
|
211
|
+
.replace(/-+/g, "-") // replace multiple hyphens with single hyphen
|
|
212
|
+
.replace(/-$/, ""); // remove trailing hyphens
|
|
192
213
|
const existingRelease = await Release.query().findOne({ slug });
|
|
193
214
|
if (existingRelease) {
|
|
194
215
|
return `${slug}-${randomStringGenerator()}`;
|
|
@@ -196,13 +217,15 @@ export default class Release extends Model {
|
|
|
196
217
|
return slug;
|
|
197
218
|
};
|
|
198
219
|
format = async () => {
|
|
199
|
-
const publisher = await this.$relatedQuery(
|
|
200
|
-
const publishedThroughHub = await this.$relatedQuery(
|
|
220
|
+
const publisher = await this.$relatedQuery("publisher");
|
|
221
|
+
const publishedThroughHub = await this.$relatedQuery("publishedThroughHub");
|
|
201
222
|
if (publishedThroughHub) {
|
|
202
223
|
this.publishedThroughHub = publishedThroughHub.publicKey;
|
|
203
224
|
this.hub = publishedThroughHub;
|
|
204
225
|
delete this.hub.id;
|
|
205
|
-
const authority = await this.hub
|
|
226
|
+
const authority = await this.hub
|
|
227
|
+
.$relatedQuery("authority")
|
|
228
|
+
.select("publicKey");
|
|
206
229
|
this.hub.authority = authority.publicKey;
|
|
207
230
|
delete this.hub.authorityId;
|
|
208
231
|
}
|
|
@@ -212,93 +235,93 @@ export default class Release extends Model {
|
|
|
212
235
|
delete this.publisherId;
|
|
213
236
|
delete this.hubId;
|
|
214
237
|
delete this.id;
|
|
215
|
-
stripHtmlIfNeeded(this.metadata,
|
|
238
|
+
stripHtmlIfNeeded(this.metadata, "description");
|
|
216
239
|
};
|
|
217
240
|
static relationMappings = () => ({
|
|
218
241
|
publishedThroughHub: {
|
|
219
242
|
relation: Model.BelongsToOneRelation,
|
|
220
243
|
modelClass: Hub,
|
|
221
244
|
join: {
|
|
222
|
-
from:
|
|
223
|
-
to:
|
|
224
|
-
}
|
|
245
|
+
from: "releases.hubId",
|
|
246
|
+
to: "hubs.id"
|
|
247
|
+
}
|
|
225
248
|
},
|
|
226
249
|
publisher: {
|
|
227
250
|
relation: Model.HasOneRelation,
|
|
228
251
|
modelClass: Account,
|
|
229
252
|
join: {
|
|
230
|
-
from:
|
|
231
|
-
to:
|
|
232
|
-
}
|
|
253
|
+
from: "releases.publisherId",
|
|
254
|
+
to: "accounts.id"
|
|
255
|
+
}
|
|
233
256
|
},
|
|
234
257
|
collectors: {
|
|
235
258
|
relation: Model.ManyToManyRelation,
|
|
236
259
|
modelClass: Account,
|
|
237
260
|
join: {
|
|
238
|
-
from:
|
|
261
|
+
from: "releases.id",
|
|
239
262
|
through: {
|
|
240
|
-
from:
|
|
241
|
-
to:
|
|
263
|
+
from: "releases_collected.releaseId",
|
|
264
|
+
to: "releases_collected.accountId"
|
|
242
265
|
},
|
|
243
|
-
to:
|
|
244
|
-
}
|
|
266
|
+
to: "accounts.id"
|
|
267
|
+
}
|
|
245
268
|
},
|
|
246
269
|
exchanges: {
|
|
247
270
|
relation: Model.HasManyRelation,
|
|
248
271
|
modelClass: Exchange,
|
|
249
272
|
join: {
|
|
250
|
-
from:
|
|
251
|
-
to:
|
|
252
|
-
}
|
|
273
|
+
from: "releases.id",
|
|
274
|
+
to: "exchanges.releaseId"
|
|
275
|
+
}
|
|
253
276
|
},
|
|
254
277
|
hubs: {
|
|
255
278
|
relation: Model.ManyToManyRelation,
|
|
256
279
|
modelClass: Hub,
|
|
257
280
|
join: {
|
|
258
|
-
from:
|
|
281
|
+
from: "releases.id",
|
|
259
282
|
through: {
|
|
260
|
-
from:
|
|
261
|
-
to:
|
|
262
|
-
extra: [
|
|
283
|
+
from: "hubs_releases.releaseId",
|
|
284
|
+
to: "hubs_releases.hubId",
|
|
285
|
+
extra: ["hubReleasePublicKey"]
|
|
263
286
|
},
|
|
264
|
-
to:
|
|
265
|
-
}
|
|
287
|
+
to: "hubs.id"
|
|
288
|
+
}
|
|
266
289
|
},
|
|
267
290
|
posts: {
|
|
268
291
|
relation: Model.ManyToManyRelation,
|
|
269
292
|
modelClass: Post,
|
|
270
293
|
join: {
|
|
271
|
-
from:
|
|
294
|
+
from: "releases.id",
|
|
272
295
|
through: {
|
|
273
|
-
from:
|
|
274
|
-
to:
|
|
296
|
+
from: "posts_releases.releaseId",
|
|
297
|
+
to: "posts_releases.postId"
|
|
275
298
|
},
|
|
276
|
-
to:
|
|
277
|
-
}
|
|
299
|
+
to: "posts.id"
|
|
300
|
+
}
|
|
278
301
|
},
|
|
279
302
|
revenueShareRecipients: {
|
|
280
303
|
relation: Model.ManyToManyRelation,
|
|
281
304
|
modelClass: Account,
|
|
282
305
|
join: {
|
|
283
|
-
from:
|
|
306
|
+
from: "releases.id",
|
|
284
307
|
through: {
|
|
285
|
-
from:
|
|
286
|
-
to:
|
|
308
|
+
from: "releases_revenue_share.releaseId",
|
|
309
|
+
to: "releases_revenue_share.accountId"
|
|
287
310
|
},
|
|
288
|
-
to:
|
|
289
|
-
}
|
|
311
|
+
to: "accounts.id"
|
|
312
|
+
}
|
|
290
313
|
},
|
|
291
314
|
tags: {
|
|
292
315
|
relation: Model.ManyToManyRelation,
|
|
293
316
|
modelClass: Tag,
|
|
294
317
|
join: {
|
|
295
|
-
from:
|
|
318
|
+
from: "releases.id",
|
|
296
319
|
through: {
|
|
297
|
-
from:
|
|
298
|
-
to:
|
|
320
|
+
from: "tags_releases.releaseId",
|
|
321
|
+
to: "tags_releases.tagId"
|
|
299
322
|
},
|
|
300
|
-
to:
|
|
301
|
-
}
|
|
302
|
-
}
|
|
323
|
+
to: "tags.id"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
303
326
|
});
|
|
304
327
|
}
|
package/package.json
CHANGED
package/src/knexfile.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* @type { Object.<string, import("knex").Knex.Config> }
|
|
3
3
|
*/
|
|
4
4
|
import "dotenv/config.js";
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
5
10
|
|
|
6
11
|
export default {
|
|
7
12
|
development: {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param { import("knex").Knex } knex
|
|
3
|
+
* @returns { Promise<void> }
|
|
4
|
+
*/
|
|
5
|
+
export async function up(knex) {
|
|
6
|
+
await knex.schema.alterTable('releases', (table) => {
|
|
7
|
+
table.string('mint').nullable().alter();
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param { import("knex").Knex } knex
|
|
13
|
+
* @returns { Promise<void> }
|
|
14
|
+
*/
|
|
15
|
+
export async function down(knex) {
|
|
16
|
+
await knex.schema.alterTable('releases', (table) => {
|
|
17
|
+
table.string('mint').notNullable().alter();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param { import("knex").Knex } knex
|
|
3
|
+
* @returns { Promise<void> }
|
|
4
|
+
*/
|
|
5
|
+
export const up = function(knex) {
|
|
6
|
+
return knex.schema.table('releases', table => {
|
|
7
|
+
table.string('solanaAddress').nullable();
|
|
8
|
+
}).then(() => {
|
|
9
|
+
return knex('releases').whereNotNull('mint').update('solanaAddress', knex.ref('publicKey'));
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param { import("knex").Knex } knex
|
|
15
|
+
* @returns { Promise<void> }
|
|
16
|
+
*/
|
|
17
|
+
export const down = function(knex) {
|
|
18
|
+
return knex.schema.table('releases', table => {
|
|
19
|
+
table.dropColumn('solanaAddress');
|
|
20
|
+
});
|
|
21
|
+
};
|
package/src/models/Release.js
CHANGED
|
@@ -14,41 +14,41 @@ import { customAlphabet } from 'nanoid';
|
|
|
14
14
|
import { getTokenMetadata } from '@solana/spl-token';
|
|
15
15
|
|
|
16
16
|
const ensureHttps = (uri) => {
|
|
17
|
-
if (!uri.startsWith(
|
|
17
|
+
if (!uri.startsWith("http://") && !uri.startsWith("https://")) {
|
|
18
18
|
return `https://${uri}`;
|
|
19
19
|
}
|
|
20
20
|
return uri;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const alphabet =
|
|
23
|
+
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
24
24
|
const randomStringGenerator = customAlphabet(alphabet, 12);
|
|
25
25
|
export default class Release extends Model {
|
|
26
|
-
static tableName =
|
|
27
|
-
|
|
28
|
-
static idColumn =
|
|
26
|
+
static tableName = "releases";
|
|
27
|
+
|
|
28
|
+
static idColumn = "id";
|
|
29
29
|
static jsonSchema = {
|
|
30
|
-
type:
|
|
31
|
-
required: [
|
|
30
|
+
type: "object",
|
|
31
|
+
required: ["metadata", "datetime", "slug", "price"],
|
|
32
32
|
properties: {
|
|
33
|
-
publicKey: { type:
|
|
34
|
-
mint: { type:
|
|
35
|
-
slug: { type:
|
|
33
|
+
publicKey: { type: ["string"] },
|
|
34
|
+
mint: { type: "string" },
|
|
35
|
+
slug: { type: "string" },
|
|
36
36
|
metadata: {
|
|
37
|
-
type:
|
|
38
|
-
required: [
|
|
37
|
+
type: "object",
|
|
38
|
+
required: ["name", "symbol", "description", "image", "properties"],
|
|
39
39
|
properties: {
|
|
40
|
-
name: { type:
|
|
41
|
-
symbol: { type:
|
|
42
|
-
description: { type:
|
|
40
|
+
name: { type: "string" },
|
|
41
|
+
symbol: { type: "string" },
|
|
42
|
+
description: { type: "string" },
|
|
43
43
|
properties: {
|
|
44
|
-
type:
|
|
44
|
+
type: "object",
|
|
45
45
|
properties: {
|
|
46
|
-
artist: { type:
|
|
47
|
-
title: { type:
|
|
48
|
-
date: { type:
|
|
49
|
-
files: { type:
|
|
50
|
-
category: { type:
|
|
51
|
-
creators: { type:
|
|
46
|
+
artist: { type: "string" },
|
|
47
|
+
title: { type: "string" },
|
|
48
|
+
date: { type: "string" },
|
|
49
|
+
files: { type: "array" },
|
|
50
|
+
category: { type: "string" },
|
|
51
|
+
creators: { type: "array" }
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -84,13 +84,14 @@ export default class Release extends Model {
|
|
|
84
84
|
const result = await program.account[programModelName].fetch(new anchor.web3.PublicKey(publicKey), 'confirmed')
|
|
85
85
|
return result
|
|
86
86
|
} catch (error) {
|
|
87
|
-
console.log(
|
|
88
|
-
retry(error)
|
|
87
|
+
console.log("error fetching release account", error);
|
|
88
|
+
retry(error);
|
|
89
89
|
}
|
|
90
|
-
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
91
92
|
retries: 50,
|
|
92
93
|
minTimeout: 500,
|
|
93
|
-
maxTimeout: 1500
|
|
94
|
+
maxTimeout: 1500
|
|
94
95
|
}
|
|
95
96
|
)
|
|
96
97
|
|
|
@@ -101,17 +102,35 @@ export default class Release extends Model {
|
|
|
101
102
|
metadataAccount = (await metaplex.nfts().findAllByMintList({mints: [releaseAccount.releaseMint]}, { commitment: 'confirmed' }))[0];
|
|
102
103
|
}
|
|
103
104
|
if (!metadataAccount) {
|
|
104
|
-
throw new Error(
|
|
105
|
+
throw new Error(
|
|
106
|
+
"No metadata account found for release - is not a complete release"
|
|
107
|
+
);
|
|
105
108
|
}
|
|
106
|
-
let json
|
|
109
|
+
let json;
|
|
107
110
|
try {
|
|
108
|
-
json = (
|
|
111
|
+
json = (
|
|
112
|
+
await axios.get(
|
|
113
|
+
ensureHttps(
|
|
114
|
+
metadataAccount.uri
|
|
115
|
+
.replace("www.", "")
|
|
116
|
+
.replace("arweave.net", "gateway.irys.xyz")
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
).data;
|
|
109
120
|
} catch (error) {
|
|
110
|
-
json = (
|
|
121
|
+
json = (
|
|
122
|
+
await axios.get(
|
|
123
|
+
ensureHttps(
|
|
124
|
+
metadataAccount.uri.replace("gateway.irys.xyz", "arweave.net")
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
).data;
|
|
111
128
|
}
|
|
112
|
-
|
|
129
|
+
|
|
113
130
|
const slug = await this.generateSlug(json);
|
|
114
|
-
let publisher = await Account.findOrCreate(
|
|
131
|
+
let publisher = await Account.findOrCreate(
|
|
132
|
+
releaseAccount.authority.toBase58()
|
|
133
|
+
);
|
|
115
134
|
release = await this.createRelease({
|
|
116
135
|
publicKey,
|
|
117
136
|
mint: programId === process.env.NINA_PROGRAM_V2_ID ? releaseAccount.mint.toBase58() : releaseAccount.releaseMint.toBase58(),
|
|
@@ -122,27 +141,34 @@ export default class Release extends Model {
|
|
|
122
141
|
releaseAccount,
|
|
123
142
|
programId,
|
|
124
143
|
});
|
|
125
|
-
|
|
144
|
+
|
|
126
145
|
if (hubPublicKey) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
const hub = await Hub.query().findOne({ publicKey: hubPublicKey });
|
|
147
|
+
await release.$query().patch({ hubId: hub.id });
|
|
148
|
+
await Hub.relatedQuery("releases")
|
|
149
|
+
.for(hub.id)
|
|
150
|
+
.patch({
|
|
151
|
+
visible: true
|
|
152
|
+
})
|
|
153
|
+
.where({ id: release.id });
|
|
133
154
|
}
|
|
134
|
-
|
|
135
|
-
return release;
|
|
155
|
+
|
|
156
|
+
return release;
|
|
136
157
|
} catch (error) {
|
|
137
|
-
console.log(
|
|
158
|
+
console.log("error finding or creating release: ", error);
|
|
138
159
|
return null;
|
|
139
160
|
}
|
|
140
|
-
}
|
|
161
|
+
};
|
|
141
162
|
|
|
142
163
|
static createRelease = async ({publicKey, mint, metadata, datetime, publisherId, releaseAccount, programId}) => {
|
|
143
164
|
const slug = await this.generateSlug(metadata);
|
|
144
|
-
const price =
|
|
145
|
-
|
|
165
|
+
const price =
|
|
166
|
+
releaseAccount.account?.price?.toNumber() ||
|
|
167
|
+
releaseAccount?.price?.toNumber() ||
|
|
168
|
+
0;
|
|
169
|
+
const paymentMint =
|
|
170
|
+
releaseAccount.account?.paymentMint.toBase58() ||
|
|
171
|
+
releaseAccount?.paymentMint.toBase58();
|
|
146
172
|
const release = await Release.query().insertGraph({
|
|
147
173
|
publicKey,
|
|
148
174
|
mint,
|
|
@@ -158,7 +184,11 @@ export default class Release extends Model {
|
|
|
158
184
|
if (metadata.properties.tags) {
|
|
159
185
|
for await (let tag of metadata.properties.tags) {
|
|
160
186
|
const tagRecord = await Tag.findOrCreate(tag);
|
|
161
|
-
await Release.relatedQuery(
|
|
187
|
+
await Release.relatedQuery("tags")
|
|
188
|
+
.for(release.id)
|
|
189
|
+
.relate(tagRecord.id)
|
|
190
|
+
.onConflict(["tagId", "releaseId"])
|
|
191
|
+
.ignore();
|
|
162
192
|
}
|
|
163
193
|
}
|
|
164
194
|
if (programId === process.env.NINA_PROGRAM_ID) {
|
|
@@ -166,156 +196,177 @@ export default class Release extends Model {
|
|
|
166
196
|
}
|
|
167
197
|
tweetNewRelease(metadata, publisherId, slug);
|
|
168
198
|
return release;
|
|
169
|
-
}
|
|
199
|
+
};
|
|
170
200
|
|
|
171
201
|
static processRevenueShares = async (releaseData, releaseRecord) => {
|
|
172
|
-
const royaltyRecipients =
|
|
202
|
+
const royaltyRecipients =
|
|
203
|
+
releaseData.account?.royaltyRecipients || releaseData.royaltyRecipients;
|
|
173
204
|
for await (let recipient of royaltyRecipients) {
|
|
174
205
|
try {
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
206
|
+
if (
|
|
207
|
+
recipient.recipientAuthority.toBase58() !==
|
|
208
|
+
"11111111111111111111111111111111"
|
|
209
|
+
) {
|
|
210
|
+
const recipientAccount = await Account.findOrCreate(
|
|
211
|
+
recipient.recipientAuthority.toBase58()
|
|
212
|
+
);
|
|
213
|
+
const revenueShares = (
|
|
214
|
+
await recipientAccount.$relatedQuery("revenueShares")
|
|
215
|
+
).map((revenueShare) => revenueShare.id);
|
|
216
|
+
if (
|
|
217
|
+
!revenueShares.includes(releaseRecord.id) &&
|
|
218
|
+
recipient.percentShare.toNumber() > 0
|
|
219
|
+
) {
|
|
220
|
+
await Account.relatedQuery("revenueShares")
|
|
221
|
+
.for(recipientAccount.id)
|
|
222
|
+
.relate(releaseRecord.id);
|
|
223
|
+
} else if (
|
|
224
|
+
revenueShares.includes(releaseRecord.id) &&
|
|
225
|
+
recipient.percentShare.toNumber() === 0
|
|
226
|
+
) {
|
|
227
|
+
await Account.relatedQuery("revenueShares")
|
|
228
|
+
.for(recipientAccount.id)
|
|
229
|
+
.unrelate()
|
|
230
|
+
.where("id", releaseRecord.id);
|
|
182
231
|
}
|
|
183
232
|
}
|
|
184
233
|
} catch (error) {
|
|
185
|
-
console.log(
|
|
234
|
+
console.log("error processing royaltyRecipients: ", error);
|
|
186
235
|
}
|
|
187
236
|
}
|
|
188
|
-
}
|
|
237
|
+
};
|
|
189
238
|
|
|
190
239
|
static generateSlug = async (metadata) => {
|
|
191
|
-
let string = metadata.name
|
|
240
|
+
let string = metadata.name;
|
|
192
241
|
if (string.length > 200) {
|
|
193
242
|
string = string.substring(0, 200);
|
|
194
243
|
}
|
|
195
244
|
const slug = string
|
|
196
|
-
.normalize(
|
|
245
|
+
.normalize("NFKD")
|
|
246
|
+
.replace(/[\u0300-\u036F]/g, "") // remove accents and convert to closest ascii equivalent
|
|
197
247
|
.toLowerCase() // convert to lowercase
|
|
198
|
-
.replace(
|
|
199
|
-
.replace(/ +/g,
|
|
200
|
-
.replace(/ /g,
|
|
201
|
-
.replace(/[^a-zA-Z0-9-]/g,
|
|
202
|
-
.replace(/-+/g,
|
|
203
|
-
.replace(/-$/,
|
|
248
|
+
.replace("-", "") // remove hyphens
|
|
249
|
+
.replace(/ +/g, " ") // remove spaces
|
|
250
|
+
.replace(/ /g, "-") // replace spaces with hyphens
|
|
251
|
+
.replace(/[^a-zA-Z0-9-]/g, "-") // replace non-alphanumeric characters with hyphens
|
|
252
|
+
.replace(/-+/g, "-") // replace multiple hyphens with single hyphen
|
|
253
|
+
.replace(/-$/, ""); // remove trailing hyphens
|
|
204
254
|
|
|
205
255
|
const existingRelease = await Release.query().findOne({ slug });
|
|
206
256
|
if (existingRelease) {
|
|
207
257
|
return `${slug}-${randomStringGenerator()}`;
|
|
208
258
|
}
|
|
209
259
|
return slug;
|
|
210
|
-
|
|
211
|
-
}
|
|
260
|
+
};
|
|
212
261
|
|
|
213
262
|
format = async () => {
|
|
214
|
-
const publisher = await this.$relatedQuery(
|
|
215
|
-
const publishedThroughHub = await this.$relatedQuery(
|
|
263
|
+
const publisher = await this.$relatedQuery("publisher");
|
|
264
|
+
const publishedThroughHub = await this.$relatedQuery("publishedThroughHub");
|
|
216
265
|
if (publishedThroughHub) {
|
|
217
266
|
this.publishedThroughHub = publishedThroughHub.publicKey;
|
|
218
267
|
this.hub = publishedThroughHub;
|
|
219
268
|
delete this.hub.id;
|
|
220
|
-
const authority = await this.hub
|
|
269
|
+
const authority = await this.hub
|
|
270
|
+
.$relatedQuery("authority")
|
|
271
|
+
.select("publicKey");
|
|
221
272
|
this.hub.authority = authority.publicKey;
|
|
222
273
|
delete this.hub.authorityId;
|
|
223
274
|
}
|
|
224
275
|
await publisher.format();
|
|
225
276
|
this.publisher = publisher.publicKey;
|
|
226
277
|
this.publisherAccount = publisher;
|
|
227
|
-
delete this.publisherId
|
|
228
|
-
delete this.hubId
|
|
229
|
-
delete this.id
|
|
278
|
+
delete this.publisherId;
|
|
279
|
+
delete this.hubId;
|
|
280
|
+
delete this.id;
|
|
230
281
|
|
|
231
|
-
stripHtmlIfNeeded(this.metadata,
|
|
232
|
-
}
|
|
282
|
+
stripHtmlIfNeeded(this.metadata, "description");
|
|
283
|
+
};
|
|
233
284
|
|
|
234
285
|
static relationMappings = () => ({
|
|
235
286
|
publishedThroughHub: {
|
|
236
287
|
relation: Model.BelongsToOneRelation,
|
|
237
288
|
modelClass: Hub,
|
|
238
289
|
join: {
|
|
239
|
-
from:
|
|
240
|
-
to:
|
|
241
|
-
}
|
|
290
|
+
from: "releases.hubId",
|
|
291
|
+
to: "hubs.id"
|
|
292
|
+
}
|
|
242
293
|
},
|
|
243
294
|
publisher: {
|
|
244
295
|
relation: Model.HasOneRelation,
|
|
245
296
|
modelClass: Account,
|
|
246
297
|
join: {
|
|
247
|
-
from:
|
|
248
|
-
to:
|
|
249
|
-
}
|
|
298
|
+
from: "releases.publisherId",
|
|
299
|
+
to: "accounts.id"
|
|
300
|
+
}
|
|
250
301
|
},
|
|
251
302
|
collectors: {
|
|
252
303
|
relation: Model.ManyToManyRelation,
|
|
253
304
|
modelClass: Account,
|
|
254
305
|
join: {
|
|
255
|
-
from:
|
|
306
|
+
from: "releases.id",
|
|
256
307
|
through: {
|
|
257
|
-
from:
|
|
258
|
-
to:
|
|
308
|
+
from: "releases_collected.releaseId",
|
|
309
|
+
to: "releases_collected.accountId"
|
|
259
310
|
},
|
|
260
|
-
to:
|
|
261
|
-
}
|
|
311
|
+
to: "accounts.id"
|
|
312
|
+
}
|
|
262
313
|
},
|
|
263
314
|
exchanges: {
|
|
264
315
|
relation: Model.HasManyRelation,
|
|
265
316
|
modelClass: Exchange,
|
|
266
317
|
join: {
|
|
267
|
-
from:
|
|
268
|
-
to:
|
|
269
|
-
}
|
|
318
|
+
from: "releases.id",
|
|
319
|
+
to: "exchanges.releaseId"
|
|
320
|
+
}
|
|
270
321
|
},
|
|
271
322
|
hubs: {
|
|
272
323
|
relation: Model.ManyToManyRelation,
|
|
273
324
|
modelClass: Hub,
|
|
274
325
|
join: {
|
|
275
|
-
from:
|
|
276
|
-
through
|
|
277
|
-
from:
|
|
278
|
-
to:
|
|
279
|
-
extra: [
|
|
326
|
+
from: "releases.id",
|
|
327
|
+
through: {
|
|
328
|
+
from: "hubs_releases.releaseId",
|
|
329
|
+
to: "hubs_releases.hubId",
|
|
330
|
+
extra: ["hubReleasePublicKey"]
|
|
280
331
|
},
|
|
281
|
-
to:
|
|
282
|
-
}
|
|
332
|
+
to: "hubs.id"
|
|
333
|
+
}
|
|
283
334
|
},
|
|
284
335
|
posts: {
|
|
285
336
|
relation: Model.ManyToManyRelation,
|
|
286
337
|
modelClass: Post,
|
|
287
338
|
join: {
|
|
288
|
-
from:
|
|
289
|
-
through
|
|
290
|
-
from:
|
|
291
|
-
to:
|
|
339
|
+
from: "releases.id",
|
|
340
|
+
through: {
|
|
341
|
+
from: "posts_releases.releaseId",
|
|
342
|
+
to: "posts_releases.postId"
|
|
292
343
|
},
|
|
293
|
-
to:
|
|
294
|
-
}
|
|
344
|
+
to: "posts.id"
|
|
345
|
+
}
|
|
295
346
|
},
|
|
296
347
|
revenueShareRecipients: {
|
|
297
348
|
relation: Model.ManyToManyRelation,
|
|
298
349
|
modelClass: Account,
|
|
299
350
|
join: {
|
|
300
|
-
from:
|
|
351
|
+
from: "releases.id",
|
|
301
352
|
through: {
|
|
302
|
-
from:
|
|
303
|
-
to:
|
|
353
|
+
from: "releases_revenue_share.releaseId",
|
|
354
|
+
to: "releases_revenue_share.accountId"
|
|
304
355
|
},
|
|
305
|
-
to:
|
|
306
|
-
}
|
|
356
|
+
to: "accounts.id"
|
|
357
|
+
}
|
|
307
358
|
},
|
|
308
359
|
tags: {
|
|
309
360
|
relation: Model.ManyToManyRelation,
|
|
310
361
|
modelClass: Tag,
|
|
311
362
|
join: {
|
|
312
|
-
from:
|
|
363
|
+
from: "releases.id",
|
|
313
364
|
through: {
|
|
314
|
-
from:
|
|
315
|
-
to:
|
|
365
|
+
from: "tags_releases.releaseId",
|
|
366
|
+
to: "tags_releases.tagId"
|
|
316
367
|
},
|
|
317
|
-
to:
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
})
|
|
368
|
+
to: "tags.id"
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
321
372
|
}
|