@nina-protocol/nina-db 0.0.106 → 0.0.108
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 +112 -88
- 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 +167 -115
- 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
|
}
|
|
@@ -54,7 +54,7 @@ export default class Release extends Model {
|
|
|
54
54
|
archived: { type: 'boolean' },
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
|
-
static findOrCreate = async (publicKey, hubPublicKey = null, programId = process.env.NINA_PROGRAM_V2_ID) => {
|
|
57
|
+
static findOrCreate = async (publicKey, hubPublicKey = null, programId = process.env.NINA_PROGRAM_V2_ID, isLazy = false) => {
|
|
58
58
|
try {
|
|
59
59
|
console.log('Release.findOrCreate', publicKey, programId);
|
|
60
60
|
let release = await Release.query().findOne({ publicKey });
|
|
@@ -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());
|
|
@@ -112,25 +114,32 @@ export default class Release extends Model {
|
|
|
112
114
|
publisherId: publisher.id,
|
|
113
115
|
releaseAccount,
|
|
114
116
|
programId,
|
|
117
|
+
solanaAddress: isLazy ? null : publicKey,
|
|
115
118
|
});
|
|
116
119
|
if (hubPublicKey) {
|
|
117
120
|
const hub = await Hub.query().findOne({ publicKey: hubPublicKey });
|
|
118
121
|
await release.$query().patch({ hubId: hub.id });
|
|
119
|
-
await Hub.relatedQuery(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
await Hub.relatedQuery("releases")
|
|
123
|
+
.for(hub.id)
|
|
124
|
+
.patch({
|
|
125
|
+
visible: true
|
|
126
|
+
})
|
|
127
|
+
.where({ id: release.id });
|
|
122
128
|
}
|
|
123
129
|
return release;
|
|
124
130
|
}
|
|
125
131
|
catch (error) {
|
|
126
|
-
console.log(
|
|
132
|
+
console.log("error finding or creating release: ", error);
|
|
127
133
|
return null;
|
|
128
134
|
}
|
|
129
135
|
};
|
|
130
136
|
static createRelease = async ({ publicKey, mint, metadata, datetime, publisherId, releaseAccount, programId }) => {
|
|
131
137
|
const slug = await this.generateSlug(metadata);
|
|
132
|
-
const price = releaseAccount.account?.price?.toNumber() ||
|
|
133
|
-
|
|
138
|
+
const price = releaseAccount.account?.price?.toNumber() ||
|
|
139
|
+
releaseAccount?.price?.toNumber() ||
|
|
140
|
+
0;
|
|
141
|
+
const paymentMint = releaseAccount.account?.paymentMint.toBase58() ||
|
|
142
|
+
releaseAccount?.paymentMint.toBase58();
|
|
134
143
|
const release = await Release.query().insertGraph({
|
|
135
144
|
publicKey,
|
|
136
145
|
mint,
|
|
@@ -146,7 +155,11 @@ export default class Release extends Model {
|
|
|
146
155
|
if (metadata.properties.tags) {
|
|
147
156
|
for await (let tag of metadata.properties.tags) {
|
|
148
157
|
const tagRecord = await Tag.findOrCreate(tag);
|
|
149
|
-
await Release.relatedQuery(
|
|
158
|
+
await Release.relatedQuery("tags")
|
|
159
|
+
.for(release.id)
|
|
160
|
+
.relate(tagRecord.id)
|
|
161
|
+
.onConflict(["tagId", "releaseId"])
|
|
162
|
+
.ignore();
|
|
150
163
|
}
|
|
151
164
|
}
|
|
152
165
|
if (programId === process.env.NINA_PROGRAM_ID) {
|
|
@@ -159,19 +172,27 @@ export default class Release extends Model {
|
|
|
159
172
|
const royaltyRecipients = releaseData.account?.royaltyRecipients || releaseData.royaltyRecipients;
|
|
160
173
|
for await (let recipient of royaltyRecipients) {
|
|
161
174
|
try {
|
|
162
|
-
if (recipient.recipientAuthority.toBase58() !==
|
|
175
|
+
if (recipient.recipientAuthority.toBase58() !==
|
|
176
|
+
"11111111111111111111111111111111") {
|
|
163
177
|
const recipientAccount = await Account.findOrCreate(recipient.recipientAuthority.toBase58());
|
|
164
|
-
const revenueShares = (await recipientAccount.$relatedQuery(
|
|
165
|
-
if (!revenueShares.includes(releaseRecord.id) &&
|
|
166
|
-
|
|
178
|
+
const revenueShares = (await recipientAccount.$relatedQuery("revenueShares")).map((revenueShare) => revenueShare.id);
|
|
179
|
+
if (!revenueShares.includes(releaseRecord.id) &&
|
|
180
|
+
recipient.percentShare.toNumber() > 0) {
|
|
181
|
+
await Account.relatedQuery("revenueShares")
|
|
182
|
+
.for(recipientAccount.id)
|
|
183
|
+
.relate(releaseRecord.id);
|
|
167
184
|
}
|
|
168
|
-
else if (revenueShares.includes(releaseRecord.id) &&
|
|
169
|
-
|
|
185
|
+
else if (revenueShares.includes(releaseRecord.id) &&
|
|
186
|
+
recipient.percentShare.toNumber() === 0) {
|
|
187
|
+
await Account.relatedQuery("revenueShares")
|
|
188
|
+
.for(recipientAccount.id)
|
|
189
|
+
.unrelate()
|
|
190
|
+
.where("id", releaseRecord.id);
|
|
170
191
|
}
|
|
171
192
|
}
|
|
172
193
|
}
|
|
173
194
|
catch (error) {
|
|
174
|
-
console.log(
|
|
195
|
+
console.log("error processing royaltyRecipients: ", error);
|
|
175
196
|
}
|
|
176
197
|
}
|
|
177
198
|
};
|
|
@@ -181,14 +202,15 @@ export default class Release extends Model {
|
|
|
181
202
|
string = string.substring(0, 200);
|
|
182
203
|
}
|
|
183
204
|
const slug = string
|
|
184
|
-
.normalize(
|
|
205
|
+
.normalize("NFKD")
|
|
206
|
+
.replace(/[\u0300-\u036F]/g, "") // remove accents and convert to closest ascii equivalent
|
|
185
207
|
.toLowerCase() // convert to lowercase
|
|
186
|
-
.replace(
|
|
187
|
-
.replace(/ +/g,
|
|
188
|
-
.replace(/ /g,
|
|
189
|
-
.replace(/[^a-zA-Z0-9-]/g,
|
|
190
|
-
.replace(/-+/g,
|
|
191
|
-
.replace(/-$/,
|
|
208
|
+
.replace("-", "") // remove hyphens
|
|
209
|
+
.replace(/ +/g, " ") // remove spaces
|
|
210
|
+
.replace(/ /g, "-") // replace spaces with hyphens
|
|
211
|
+
.replace(/[^a-zA-Z0-9-]/g, "-") // replace non-alphanumeric characters with hyphens
|
|
212
|
+
.replace(/-+/g, "-") // replace multiple hyphens with single hyphen
|
|
213
|
+
.replace(/-$/, ""); // remove trailing hyphens
|
|
192
214
|
const existingRelease = await Release.query().findOne({ slug });
|
|
193
215
|
if (existingRelease) {
|
|
194
216
|
return `${slug}-${randomStringGenerator()}`;
|
|
@@ -196,13 +218,15 @@ export default class Release extends Model {
|
|
|
196
218
|
return slug;
|
|
197
219
|
};
|
|
198
220
|
format = async () => {
|
|
199
|
-
const publisher = await this.$relatedQuery(
|
|
200
|
-
const publishedThroughHub = await this.$relatedQuery(
|
|
221
|
+
const publisher = await this.$relatedQuery("publisher");
|
|
222
|
+
const publishedThroughHub = await this.$relatedQuery("publishedThroughHub");
|
|
201
223
|
if (publishedThroughHub) {
|
|
202
224
|
this.publishedThroughHub = publishedThroughHub.publicKey;
|
|
203
225
|
this.hub = publishedThroughHub;
|
|
204
226
|
delete this.hub.id;
|
|
205
|
-
const authority = await this.hub
|
|
227
|
+
const authority = await this.hub
|
|
228
|
+
.$relatedQuery("authority")
|
|
229
|
+
.select("publicKey");
|
|
206
230
|
this.hub.authority = authority.publicKey;
|
|
207
231
|
delete this.hub.authorityId;
|
|
208
232
|
}
|
|
@@ -212,93 +236,93 @@ export default class Release extends Model {
|
|
|
212
236
|
delete this.publisherId;
|
|
213
237
|
delete this.hubId;
|
|
214
238
|
delete this.id;
|
|
215
|
-
stripHtmlIfNeeded(this.metadata,
|
|
239
|
+
stripHtmlIfNeeded(this.metadata, "description");
|
|
216
240
|
};
|
|
217
241
|
static relationMappings = () => ({
|
|
218
242
|
publishedThroughHub: {
|
|
219
243
|
relation: Model.BelongsToOneRelation,
|
|
220
244
|
modelClass: Hub,
|
|
221
245
|
join: {
|
|
222
|
-
from:
|
|
223
|
-
to:
|
|
224
|
-
}
|
|
246
|
+
from: "releases.hubId",
|
|
247
|
+
to: "hubs.id"
|
|
248
|
+
}
|
|
225
249
|
},
|
|
226
250
|
publisher: {
|
|
227
251
|
relation: Model.HasOneRelation,
|
|
228
252
|
modelClass: Account,
|
|
229
253
|
join: {
|
|
230
|
-
from:
|
|
231
|
-
to:
|
|
232
|
-
}
|
|
254
|
+
from: "releases.publisherId",
|
|
255
|
+
to: "accounts.id"
|
|
256
|
+
}
|
|
233
257
|
},
|
|
234
258
|
collectors: {
|
|
235
259
|
relation: Model.ManyToManyRelation,
|
|
236
260
|
modelClass: Account,
|
|
237
261
|
join: {
|
|
238
|
-
from:
|
|
262
|
+
from: "releases.id",
|
|
239
263
|
through: {
|
|
240
|
-
from:
|
|
241
|
-
to:
|
|
264
|
+
from: "releases_collected.releaseId",
|
|
265
|
+
to: "releases_collected.accountId"
|
|
242
266
|
},
|
|
243
|
-
to:
|
|
244
|
-
}
|
|
267
|
+
to: "accounts.id"
|
|
268
|
+
}
|
|
245
269
|
},
|
|
246
270
|
exchanges: {
|
|
247
271
|
relation: Model.HasManyRelation,
|
|
248
272
|
modelClass: Exchange,
|
|
249
273
|
join: {
|
|
250
|
-
from:
|
|
251
|
-
to:
|
|
252
|
-
}
|
|
274
|
+
from: "releases.id",
|
|
275
|
+
to: "exchanges.releaseId"
|
|
276
|
+
}
|
|
253
277
|
},
|
|
254
278
|
hubs: {
|
|
255
279
|
relation: Model.ManyToManyRelation,
|
|
256
280
|
modelClass: Hub,
|
|
257
281
|
join: {
|
|
258
|
-
from:
|
|
282
|
+
from: "releases.id",
|
|
259
283
|
through: {
|
|
260
|
-
from:
|
|
261
|
-
to:
|
|
262
|
-
extra: [
|
|
284
|
+
from: "hubs_releases.releaseId",
|
|
285
|
+
to: "hubs_releases.hubId",
|
|
286
|
+
extra: ["hubReleasePublicKey"]
|
|
263
287
|
},
|
|
264
|
-
to:
|
|
265
|
-
}
|
|
288
|
+
to: "hubs.id"
|
|
289
|
+
}
|
|
266
290
|
},
|
|
267
291
|
posts: {
|
|
268
292
|
relation: Model.ManyToManyRelation,
|
|
269
293
|
modelClass: Post,
|
|
270
294
|
join: {
|
|
271
|
-
from:
|
|
295
|
+
from: "releases.id",
|
|
272
296
|
through: {
|
|
273
|
-
from:
|
|
274
|
-
to:
|
|
297
|
+
from: "posts_releases.releaseId",
|
|
298
|
+
to: "posts_releases.postId"
|
|
275
299
|
},
|
|
276
|
-
to:
|
|
277
|
-
}
|
|
300
|
+
to: "posts.id"
|
|
301
|
+
}
|
|
278
302
|
},
|
|
279
303
|
revenueShareRecipients: {
|
|
280
304
|
relation: Model.ManyToManyRelation,
|
|
281
305
|
modelClass: Account,
|
|
282
306
|
join: {
|
|
283
|
-
from:
|
|
307
|
+
from: "releases.id",
|
|
284
308
|
through: {
|
|
285
|
-
from:
|
|
286
|
-
to:
|
|
309
|
+
from: "releases_revenue_share.releaseId",
|
|
310
|
+
to: "releases_revenue_share.accountId"
|
|
287
311
|
},
|
|
288
|
-
to:
|
|
289
|
-
}
|
|
312
|
+
to: "accounts.id"
|
|
313
|
+
}
|
|
290
314
|
},
|
|
291
315
|
tags: {
|
|
292
316
|
relation: Model.ManyToManyRelation,
|
|
293
317
|
modelClass: Tag,
|
|
294
318
|
join: {
|
|
295
|
-
from:
|
|
319
|
+
from: "releases.id",
|
|
296
320
|
through: {
|
|
297
|
-
from:
|
|
298
|
-
to:
|
|
321
|
+
from: "tags_releases.releaseId",
|
|
322
|
+
to: "tags_releases.tagId"
|
|
299
323
|
},
|
|
300
|
-
to:
|
|
301
|
-
}
|
|
302
|
-
}
|
|
324
|
+
to: "tags.id"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
303
327
|
});
|
|
304
328
|
}
|
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
|
}
|
|
@@ -58,7 +58,7 @@ export default class Release extends Model {
|
|
|
58
58
|
},
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
static findOrCreate = async (publicKey, hubPublicKey=null, programId=process.env.NINA_PROGRAM_V2_ID) => {
|
|
61
|
+
static findOrCreate = async (publicKey, hubPublicKey=null, programId=process.env.NINA_PROGRAM_V2_ID, isLazy=false) => {
|
|
62
62
|
try {
|
|
63
63
|
console.log('Release.findOrCreate', publicKey, programId)
|
|
64
64
|
let release = await Release.query().findOne({ publicKey });
|
|
@@ -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(),
|
|
@@ -121,28 +140,36 @@ export default class Release extends Model {
|
|
|
121
140
|
publisherId: publisher.id,
|
|
122
141
|
releaseAccount,
|
|
123
142
|
programId,
|
|
143
|
+
solanaAddress: isLazy ? null : publicKey,
|
|
124
144
|
});
|
|
125
|
-
|
|
145
|
+
|
|
126
146
|
if (hubPublicKey) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
const hub = await Hub.query().findOne({ publicKey: hubPublicKey });
|
|
148
|
+
await release.$query().patch({ hubId: hub.id });
|
|
149
|
+
await Hub.relatedQuery("releases")
|
|
150
|
+
.for(hub.id)
|
|
151
|
+
.patch({
|
|
152
|
+
visible: true
|
|
153
|
+
})
|
|
154
|
+
.where({ id: release.id });
|
|
133
155
|
}
|
|
134
|
-
|
|
135
|
-
return release;
|
|
156
|
+
|
|
157
|
+
return release;
|
|
136
158
|
} catch (error) {
|
|
137
|
-
console.log(
|
|
159
|
+
console.log("error finding or creating release: ", error);
|
|
138
160
|
return null;
|
|
139
161
|
}
|
|
140
|
-
}
|
|
162
|
+
};
|
|
141
163
|
|
|
142
164
|
static createRelease = async ({publicKey, mint, metadata, datetime, publisherId, releaseAccount, programId}) => {
|
|
143
165
|
const slug = await this.generateSlug(metadata);
|
|
144
|
-
const price =
|
|
145
|
-
|
|
166
|
+
const price =
|
|
167
|
+
releaseAccount.account?.price?.toNumber() ||
|
|
168
|
+
releaseAccount?.price?.toNumber() ||
|
|
169
|
+
0;
|
|
170
|
+
const paymentMint =
|
|
171
|
+
releaseAccount.account?.paymentMint.toBase58() ||
|
|
172
|
+
releaseAccount?.paymentMint.toBase58();
|
|
146
173
|
const release = await Release.query().insertGraph({
|
|
147
174
|
publicKey,
|
|
148
175
|
mint,
|
|
@@ -158,7 +185,11 @@ export default class Release extends Model {
|
|
|
158
185
|
if (metadata.properties.tags) {
|
|
159
186
|
for await (let tag of metadata.properties.tags) {
|
|
160
187
|
const tagRecord = await Tag.findOrCreate(tag);
|
|
161
|
-
await Release.relatedQuery(
|
|
188
|
+
await Release.relatedQuery("tags")
|
|
189
|
+
.for(release.id)
|
|
190
|
+
.relate(tagRecord.id)
|
|
191
|
+
.onConflict(["tagId", "releaseId"])
|
|
192
|
+
.ignore();
|
|
162
193
|
}
|
|
163
194
|
}
|
|
164
195
|
if (programId === process.env.NINA_PROGRAM_ID) {
|
|
@@ -166,156 +197,177 @@ export default class Release extends Model {
|
|
|
166
197
|
}
|
|
167
198
|
tweetNewRelease(metadata, publisherId, slug);
|
|
168
199
|
return release;
|
|
169
|
-
}
|
|
200
|
+
};
|
|
170
201
|
|
|
171
202
|
static processRevenueShares = async (releaseData, releaseRecord) => {
|
|
172
|
-
const royaltyRecipients =
|
|
203
|
+
const royaltyRecipients =
|
|
204
|
+
releaseData.account?.royaltyRecipients || releaseData.royaltyRecipients;
|
|
173
205
|
for await (let recipient of royaltyRecipients) {
|
|
174
206
|
try {
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
207
|
+
if (
|
|
208
|
+
recipient.recipientAuthority.toBase58() !==
|
|
209
|
+
"11111111111111111111111111111111"
|
|
210
|
+
) {
|
|
211
|
+
const recipientAccount = await Account.findOrCreate(
|
|
212
|
+
recipient.recipientAuthority.toBase58()
|
|
213
|
+
);
|
|
214
|
+
const revenueShares = (
|
|
215
|
+
await recipientAccount.$relatedQuery("revenueShares")
|
|
216
|
+
).map((revenueShare) => revenueShare.id);
|
|
217
|
+
if (
|
|
218
|
+
!revenueShares.includes(releaseRecord.id) &&
|
|
219
|
+
recipient.percentShare.toNumber() > 0
|
|
220
|
+
) {
|
|
221
|
+
await Account.relatedQuery("revenueShares")
|
|
222
|
+
.for(recipientAccount.id)
|
|
223
|
+
.relate(releaseRecord.id);
|
|
224
|
+
} else if (
|
|
225
|
+
revenueShares.includes(releaseRecord.id) &&
|
|
226
|
+
recipient.percentShare.toNumber() === 0
|
|
227
|
+
) {
|
|
228
|
+
await Account.relatedQuery("revenueShares")
|
|
229
|
+
.for(recipientAccount.id)
|
|
230
|
+
.unrelate()
|
|
231
|
+
.where("id", releaseRecord.id);
|
|
182
232
|
}
|
|
183
233
|
}
|
|
184
234
|
} catch (error) {
|
|
185
|
-
console.log(
|
|
235
|
+
console.log("error processing royaltyRecipients: ", error);
|
|
186
236
|
}
|
|
187
237
|
}
|
|
188
|
-
}
|
|
238
|
+
};
|
|
189
239
|
|
|
190
240
|
static generateSlug = async (metadata) => {
|
|
191
|
-
let string = metadata.name
|
|
241
|
+
let string = metadata.name;
|
|
192
242
|
if (string.length > 200) {
|
|
193
243
|
string = string.substring(0, 200);
|
|
194
244
|
}
|
|
195
245
|
const slug = string
|
|
196
|
-
.normalize(
|
|
246
|
+
.normalize("NFKD")
|
|
247
|
+
.replace(/[\u0300-\u036F]/g, "") // remove accents and convert to closest ascii equivalent
|
|
197
248
|
.toLowerCase() // convert to lowercase
|
|
198
|
-
.replace(
|
|
199
|
-
.replace(/ +/g,
|
|
200
|
-
.replace(/ /g,
|
|
201
|
-
.replace(/[^a-zA-Z0-9-]/g,
|
|
202
|
-
.replace(/-+/g,
|
|
203
|
-
.replace(/-$/,
|
|
249
|
+
.replace("-", "") // remove hyphens
|
|
250
|
+
.replace(/ +/g, " ") // remove spaces
|
|
251
|
+
.replace(/ /g, "-") // replace spaces with hyphens
|
|
252
|
+
.replace(/[^a-zA-Z0-9-]/g, "-") // replace non-alphanumeric characters with hyphens
|
|
253
|
+
.replace(/-+/g, "-") // replace multiple hyphens with single hyphen
|
|
254
|
+
.replace(/-$/, ""); // remove trailing hyphens
|
|
204
255
|
|
|
205
256
|
const existingRelease = await Release.query().findOne({ slug });
|
|
206
257
|
if (existingRelease) {
|
|
207
258
|
return `${slug}-${randomStringGenerator()}`;
|
|
208
259
|
}
|
|
209
260
|
return slug;
|
|
210
|
-
|
|
211
|
-
}
|
|
261
|
+
};
|
|
212
262
|
|
|
213
263
|
format = async () => {
|
|
214
|
-
const publisher = await this.$relatedQuery(
|
|
215
|
-
const publishedThroughHub = await this.$relatedQuery(
|
|
264
|
+
const publisher = await this.$relatedQuery("publisher");
|
|
265
|
+
const publishedThroughHub = await this.$relatedQuery("publishedThroughHub");
|
|
216
266
|
if (publishedThroughHub) {
|
|
217
267
|
this.publishedThroughHub = publishedThroughHub.publicKey;
|
|
218
268
|
this.hub = publishedThroughHub;
|
|
219
269
|
delete this.hub.id;
|
|
220
|
-
const authority = await this.hub
|
|
270
|
+
const authority = await this.hub
|
|
271
|
+
.$relatedQuery("authority")
|
|
272
|
+
.select("publicKey");
|
|
221
273
|
this.hub.authority = authority.publicKey;
|
|
222
274
|
delete this.hub.authorityId;
|
|
223
275
|
}
|
|
224
276
|
await publisher.format();
|
|
225
277
|
this.publisher = publisher.publicKey;
|
|
226
278
|
this.publisherAccount = publisher;
|
|
227
|
-
delete this.publisherId
|
|
228
|
-
delete this.hubId
|
|
229
|
-
delete this.id
|
|
279
|
+
delete this.publisherId;
|
|
280
|
+
delete this.hubId;
|
|
281
|
+
delete this.id;
|
|
230
282
|
|
|
231
|
-
stripHtmlIfNeeded(this.metadata,
|
|
232
|
-
}
|
|
283
|
+
stripHtmlIfNeeded(this.metadata, "description");
|
|
284
|
+
};
|
|
233
285
|
|
|
234
286
|
static relationMappings = () => ({
|
|
235
287
|
publishedThroughHub: {
|
|
236
288
|
relation: Model.BelongsToOneRelation,
|
|
237
289
|
modelClass: Hub,
|
|
238
290
|
join: {
|
|
239
|
-
from:
|
|
240
|
-
to:
|
|
241
|
-
}
|
|
291
|
+
from: "releases.hubId",
|
|
292
|
+
to: "hubs.id"
|
|
293
|
+
}
|
|
242
294
|
},
|
|
243
295
|
publisher: {
|
|
244
296
|
relation: Model.HasOneRelation,
|
|
245
297
|
modelClass: Account,
|
|
246
298
|
join: {
|
|
247
|
-
from:
|
|
248
|
-
to:
|
|
249
|
-
}
|
|
299
|
+
from: "releases.publisherId",
|
|
300
|
+
to: "accounts.id"
|
|
301
|
+
}
|
|
250
302
|
},
|
|
251
303
|
collectors: {
|
|
252
304
|
relation: Model.ManyToManyRelation,
|
|
253
305
|
modelClass: Account,
|
|
254
306
|
join: {
|
|
255
|
-
from:
|
|
307
|
+
from: "releases.id",
|
|
256
308
|
through: {
|
|
257
|
-
from:
|
|
258
|
-
to:
|
|
309
|
+
from: "releases_collected.releaseId",
|
|
310
|
+
to: "releases_collected.accountId"
|
|
259
311
|
},
|
|
260
|
-
to:
|
|
261
|
-
}
|
|
312
|
+
to: "accounts.id"
|
|
313
|
+
}
|
|
262
314
|
},
|
|
263
315
|
exchanges: {
|
|
264
316
|
relation: Model.HasManyRelation,
|
|
265
317
|
modelClass: Exchange,
|
|
266
318
|
join: {
|
|
267
|
-
from:
|
|
268
|
-
to:
|
|
269
|
-
}
|
|
319
|
+
from: "releases.id",
|
|
320
|
+
to: "exchanges.releaseId"
|
|
321
|
+
}
|
|
270
322
|
},
|
|
271
323
|
hubs: {
|
|
272
324
|
relation: Model.ManyToManyRelation,
|
|
273
325
|
modelClass: Hub,
|
|
274
326
|
join: {
|
|
275
|
-
from:
|
|
276
|
-
through
|
|
277
|
-
from:
|
|
278
|
-
to:
|
|
279
|
-
extra: [
|
|
327
|
+
from: "releases.id",
|
|
328
|
+
through: {
|
|
329
|
+
from: "hubs_releases.releaseId",
|
|
330
|
+
to: "hubs_releases.hubId",
|
|
331
|
+
extra: ["hubReleasePublicKey"]
|
|
280
332
|
},
|
|
281
|
-
to:
|
|
282
|
-
}
|
|
333
|
+
to: "hubs.id"
|
|
334
|
+
}
|
|
283
335
|
},
|
|
284
336
|
posts: {
|
|
285
337
|
relation: Model.ManyToManyRelation,
|
|
286
338
|
modelClass: Post,
|
|
287
339
|
join: {
|
|
288
|
-
from:
|
|
289
|
-
through
|
|
290
|
-
from:
|
|
291
|
-
to:
|
|
340
|
+
from: "releases.id",
|
|
341
|
+
through: {
|
|
342
|
+
from: "posts_releases.releaseId",
|
|
343
|
+
to: "posts_releases.postId"
|
|
292
344
|
},
|
|
293
|
-
to:
|
|
294
|
-
}
|
|
345
|
+
to: "posts.id"
|
|
346
|
+
}
|
|
295
347
|
},
|
|
296
348
|
revenueShareRecipients: {
|
|
297
349
|
relation: Model.ManyToManyRelation,
|
|
298
350
|
modelClass: Account,
|
|
299
351
|
join: {
|
|
300
|
-
from:
|
|
352
|
+
from: "releases.id",
|
|
301
353
|
through: {
|
|
302
|
-
from:
|
|
303
|
-
to:
|
|
354
|
+
from: "releases_revenue_share.releaseId",
|
|
355
|
+
to: "releases_revenue_share.accountId"
|
|
304
356
|
},
|
|
305
|
-
to:
|
|
306
|
-
}
|
|
357
|
+
to: "accounts.id"
|
|
358
|
+
}
|
|
307
359
|
},
|
|
308
360
|
tags: {
|
|
309
361
|
relation: Model.ManyToManyRelation,
|
|
310
362
|
modelClass: Tag,
|
|
311
363
|
join: {
|
|
312
|
-
from:
|
|
364
|
+
from: "releases.id",
|
|
313
365
|
through: {
|
|
314
|
-
from:
|
|
315
|
-
to:
|
|
366
|
+
from: "tags_releases.releaseId",
|
|
367
|
+
to: "tags_releases.tagId"
|
|
316
368
|
},
|
|
317
|
-
to:
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
})
|
|
369
|
+
to: "tags.id"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
321
373
|
}
|