@centrifuge/sdk 1.16.0 → 1.18.0
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/Centrifuge.d.ts.map +1 -1
- package/dist/Centrifuge.js +1 -0
- package/dist/Centrifuge.js.map +1 -1
- package/dist/entities/Pool.d.ts.map +1 -1
- package/dist/entities/Pool.js +1 -5
- package/dist/entities/Pool.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tests/mocks/mockPoolMetadataV2.d.ts.map +1 -1
- package/dist/tests/mocks/mockPoolMetadataV2.js +122 -17
- package/dist/tests/mocks/mockPoolMetadataV2.js.map +1 -1
- package/dist/types/poolInput.d.ts +3 -5
- package/dist/types/poolInput.d.ts.map +1 -1
- package/dist/types/poolMetadata.d.ts +173 -26
- package/dist/types/poolMetadata.d.ts.map +1 -1
- package/dist/types/poolMetadata.js.map +1 -1
- package/dist/utils/poolMetadataMigration.d.ts +18 -5
- package/dist/utils/poolMetadataMigration.d.ts.map +1 -1
- package/dist/utils/poolMetadataMigration.js +497 -67
- package/dist/utils/poolMetadataMigration.js.map +1 -1
- package/dist/utils/poolMetadataMigration.test.js +459 -38
- package/dist/utils/poolMetadataMigration.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { expect } from 'chai';
|
|
|
2
2
|
import { mockPoolMetadata } from '../tests/mocks/mockPoolMetadata.js';
|
|
3
3
|
import { mockPoolMetadataV2 } from '../tests/mocks/mockPoolMetadataV2.js';
|
|
4
4
|
import { isPoolMetadataV2 } from '../types/poolMetadata.js';
|
|
5
|
-
import { migratePoolMetadataToV2, parsePoolMetadataV2 } from './poolMetadataMigration.js';
|
|
5
|
+
import { migratePoolMetadataToV2, parsePoolMetadataV2, resolveLinkTarget } from './poolMetadataMigration.js';
|
|
6
6
|
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
7
7
|
/** A legacy fixture with the display fields that drive the factsheet projection. */
|
|
8
8
|
function legacyFixture() {
|
|
@@ -24,8 +24,10 @@ function legacyFixture() {
|
|
|
24
24
|
logo: null,
|
|
25
25
|
shortDescription: 'Acme',
|
|
26
26
|
categories: [
|
|
27
|
-
{ type: '
|
|
28
|
-
{ type: '
|
|
27
|
+
{ type: 'Investment Manager', value: 'Anemoy Asset Management Ltd.' }, // absorbed by Issuer (dropped)
|
|
28
|
+
{ type: 'Sub-Investment Manager', value: 'Janus Henderson Investors' }, // -> Portfolio Manager (SP)
|
|
29
|
+
{ type: 'Fund Administrator', value: 'Trident Trust Company (Cayman) Ltd' }, // -> Service providers
|
|
30
|
+
{ type: 'Auditor', value: 'MHA Cayman ' }, // -> Service providers (trailing space, trimmed on migrate)
|
|
29
31
|
],
|
|
30
32
|
},
|
|
31
33
|
links: { executiveSummary: null },
|
|
@@ -64,6 +66,11 @@ function legacyFixture() {
|
|
|
64
66
|
loanTemplates: [{ id: 'tpl', createdAt: '2024-01-01' }],
|
|
65
67
|
addressLabels: { '0xabc': 'Treasury' },
|
|
66
68
|
workflowPolicies: [],
|
|
69
|
+
withdrawManagers: {
|
|
70
|
+
'0x6756e091ae798a8e51e12e27ee8facdf': [
|
|
71
|
+
{ assetAddress: '0xa0b8', chainId: '1', manager: '0x1a3b', label: 'C-AC' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
67
74
|
};
|
|
68
75
|
}
|
|
69
76
|
describe('migratePoolMetadataToV2', () => {
|
|
@@ -93,64 +100,226 @@ describe('migratePoolMetadataToV2', () => {
|
|
|
93
100
|
expect(v2.shareClasses['0x6756e091ae798a8e51e12e27ee8facdf'].apy).to.equal('fixed');
|
|
94
101
|
expect(v2.shareClasses['0xda64aae939e4d3a981004619f1709d8f'].apy).to.equal('90d365');
|
|
95
102
|
});
|
|
96
|
-
it('preserves engine fields verbatim', () => {
|
|
103
|
+
it('preserves engine fields verbatim (incl. withdrawManagers)', () => {
|
|
97
104
|
const v2 = migratePoolMetadataToV2(legacyFixture());
|
|
98
105
|
expect(v2.addressLabels).to.deep.equal({ '0xabc': 'Treasury' });
|
|
99
106
|
expect(v2.workflowPolicies).to.deep.equal([]);
|
|
107
|
+
expect(v2.withdrawManagers).to.deep.equal({
|
|
108
|
+
'0x6756e091ae798a8e51e12e27ee8facdf': [
|
|
109
|
+
{ assetAddress: '0xa0b8', chainId: '1', manager: '0x1a3b', label: 'C-AC' },
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
it('carries pool.links.documents through migration', () => {
|
|
114
|
+
const legacy = legacyFixture();
|
|
115
|
+
legacy.pool.links = {
|
|
116
|
+
executiveSummary: null,
|
|
117
|
+
documents: [{ key: 'tos', label: 'Terms', href: 'https://x.io/tos' }],
|
|
118
|
+
};
|
|
119
|
+
const v2 = migratePoolMetadataToV2(legacy);
|
|
120
|
+
expect(v2.pool.links.documents).to.deep.equal([{ key: 'tos', label: 'Terms', href: 'https://x.io/tos' }]);
|
|
121
|
+
});
|
|
122
|
+
it('drops a stray pool.report (singular) non-schema field', () => {
|
|
123
|
+
const legacy = legacyFixture();
|
|
124
|
+
legacy.pool.report = { author: { name: '', title: '', avatar: null }, url: '' };
|
|
125
|
+
const v2 = migratePoolMetadataToV2(legacy);
|
|
126
|
+
expect(v2.pool).to.not.have.property('report');
|
|
127
|
+
});
|
|
128
|
+
it('trims whitespace from migrated text values', () => {
|
|
129
|
+
// legacyFixture's Auditor value carries a trailing space.
|
|
130
|
+
const serviceProviders = migratePoolMetadataToV2(legacyFixture()).pool.factsheet.keyFacts.find((g) => g.id === 'service-providers');
|
|
131
|
+
const auditor = serviceProviders.items.find((i) => i.label === 'Auditor');
|
|
132
|
+
expect(auditor.value).to.deep.equal({ kind: 'text', text: 'MHA Cayman' });
|
|
100
133
|
});
|
|
101
|
-
it('
|
|
134
|
+
it('preserves poolRatings verbatim as a typed field (incl. reportFile)', () => {
|
|
102
135
|
const v2 = migratePoolMetadataToV2(legacyFixture());
|
|
103
136
|
expect(v2.pool.poolRatings).to.deep.equal([
|
|
104
|
-
{
|
|
137
|
+
{
|
|
138
|
+
agency: 'Moodys',
|
|
139
|
+
value: 'Aaa',
|
|
140
|
+
reportUrl: 'https://x.io/r1',
|
|
141
|
+
reportFile: { uri: 'ipfs://Qm1', mime: 'application/pdf' },
|
|
142
|
+
},
|
|
105
143
|
{ agency: 'S&P', value: 'AAA' },
|
|
106
144
|
]);
|
|
107
145
|
});
|
|
108
|
-
it('projects
|
|
146
|
+
it('projects a titled "Key facts" group (incl. seeded wallet infra + networks) and a "Service providers" group', () => {
|
|
109
147
|
const { keyFacts } = migratePoolMetadataToV2(legacyFixture()).pool.factsheet;
|
|
110
148
|
expect(keyFacts).to.deep.equal([
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
149
|
+
{
|
|
150
|
+
type: 'keyFactGroup',
|
|
151
|
+
id: 'key-facts',
|
|
152
|
+
title: 'Key facts',
|
|
153
|
+
items: [
|
|
154
|
+
{ label: 'Issuer', value: { kind: 'text', text: 'Acme Issuer' } },
|
|
155
|
+
{ label: 'Asset type', value: { kind: 'text', text: 'Public credit - T-Bills' } },
|
|
156
|
+
{ label: 'APY', value: { kind: 'ref', ref: 'apy' } },
|
|
157
|
+
{ label: 'Pool structure', value: { kind: 'text', text: 'revolving' } },
|
|
158
|
+
{ label: 'Average asset maturity', value: { kind: 'text', text: '63 days' } },
|
|
159
|
+
{ label: 'Expense ratio', value: { kind: 'text', text: '0.25%' } },
|
|
160
|
+
{ label: 'Min. investment', value: { kind: 'text', text: '2,500' } },
|
|
161
|
+
// Investment Manager is absorbed into Issuer (no row); the other three are service providers.
|
|
162
|
+
{ label: 'Wallet infrastructure', value: { kind: 'icons', icons: [{ source: 'app', key: 'fordefi' }] } },
|
|
163
|
+
{ label: 'Available networks', value: { kind: 'ref', ref: 'availableNetworks' } },
|
|
164
|
+
{ label: 'Ratings', value: { kind: 'ref', ref: 'ratings' } },
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: 'keyFactGroup',
|
|
169
|
+
id: 'service-providers',
|
|
170
|
+
title: 'Service providers',
|
|
171
|
+
items: [
|
|
172
|
+
{ label: 'Portfolio Manager', value: { kind: 'text', text: 'Janus Henderson Investors' } },
|
|
173
|
+
{ label: 'Fund Administrator', value: { kind: 'text', text: 'Trident Trust Company (Cayman) Ltd' } },
|
|
174
|
+
{ label: 'Auditor', value: { kind: 'text', text: 'MHA Cayman' } },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
120
177
|
]);
|
|
121
178
|
});
|
|
122
|
-
it('
|
|
179
|
+
it('absorbs the Investment Manager category into the Issuer key fact (drops its own row)', () => {
|
|
180
|
+
const { keyFacts } = migratePoolMetadataToV2(legacyFixture()).pool.factsheet;
|
|
181
|
+
const allItems = keyFacts.flatMap((g) => g.items);
|
|
182
|
+
expect(allItems.filter((i) => i.label === 'Issuer')).to.have.length(1);
|
|
183
|
+
expect(allItems.some((i) => i.value.kind === 'text' && i.value.text === 'Anemoy Asset Management Ltd.')).to.equal(false);
|
|
184
|
+
});
|
|
185
|
+
it('seeds wallet infrastructure + available networks even with no categories or ratings', () => {
|
|
186
|
+
const legacy = legacyFixture();
|
|
187
|
+
legacy.pool.issuer.categories = [];
|
|
188
|
+
legacy.pool.poolRatings = [];
|
|
189
|
+
const { keyFacts } = migratePoolMetadataToV2(legacy).pool.factsheet;
|
|
190
|
+
expect(keyFacts).to.have.length(1); // no Service providers group
|
|
191
|
+
const labels = keyFacts[0].items.map((i) => i.label);
|
|
192
|
+
expect(labels).to.include('Wallet infrastructure');
|
|
193
|
+
expect(labels).to.include('Available networks');
|
|
194
|
+
expect(labels).to.not.include('Ratings');
|
|
195
|
+
});
|
|
196
|
+
it('migrates cleanly when issuer.categories is absent (real legacy docs omit it)', () => {
|
|
197
|
+
const legacy = legacyFixture();
|
|
198
|
+
delete legacy.pool.issuer.categories;
|
|
199
|
+
expect(() => migratePoolMetadataToV2(legacy)).to.not.throw();
|
|
200
|
+
const { keyFacts } = migratePoolMetadataToV2(legacy).pool.factsheet;
|
|
201
|
+
expect(keyFacts).to.have.length(1); // Key facts only, no Service providers group
|
|
202
|
+
});
|
|
203
|
+
it('keeps unknown categories in "Key facts" and omits the Service providers group when none match', () => {
|
|
204
|
+
const legacy = legacyFixture();
|
|
205
|
+
legacy.pool.issuer.categories = [{ type: 'Historical default rate', value: '0%' }];
|
|
206
|
+
const { keyFacts } = migratePoolMetadataToV2(legacy).pool.factsheet;
|
|
207
|
+
expect(keyFacts).to.have.length(1);
|
|
208
|
+
expect(keyFacts[0].title).to.equal('Key facts');
|
|
209
|
+
expect(keyFacts[0].items.some((i) => i.label === 'Historical default rate')).to.equal(true);
|
|
210
|
+
});
|
|
211
|
+
it('matches service-provider category types case- and separator-insensitively', () => {
|
|
212
|
+
const legacy = legacyFixture();
|
|
213
|
+
legacy.pool.issuer.categories = [{ type: 'Fund_Administrator', value: 'Trident Trust' }];
|
|
214
|
+
const groups = migratePoolMetadataToV2(legacy).pool.factsheet.keyFacts;
|
|
215
|
+
const serviceProviders = groups.find((g) => g.id === 'service-providers');
|
|
216
|
+
expect(serviceProviders?.items).to.deep.equal([
|
|
217
|
+
{ label: 'Fund Administrator', value: { kind: 'text', text: 'Trident Trust' } },
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
it('projects description and details into body blocks (no Documents block in body)', () => {
|
|
123
221
|
const { body } = migratePoolMetadataToV2(legacyFixture()).pool.factsheet;
|
|
124
222
|
expect(body).to.deep.equal([
|
|
125
223
|
{ type: 'text', id: 'overview', title: 'Overview', body: 'A long issuer description.' },
|
|
126
224
|
{ type: 'text', id: 'detail-0', title: 'Strategy', body: 'We do things.' },
|
|
127
225
|
{ type: 'text', id: 'detail-1', title: 'Risks', body: 'Things can go wrong.' },
|
|
128
|
-
{ type: 'text', id: 'documents', title: 'Documents', body: '- [Moodys rating report](ipfs://Qm1)' },
|
|
129
226
|
{ type: 'section', id: 'performance', ref: 'onchainMetrics' },
|
|
130
227
|
]);
|
|
131
228
|
});
|
|
132
|
-
it('
|
|
229
|
+
it('builds the Overview card with logo and a Factsheet linkRef when those source fields exist', () => {
|
|
230
|
+
const legacy = legacyFixture();
|
|
231
|
+
legacy.pool.issuer.logo = { uri: 'ipfs://QmLogo', mime: 'image/png' };
|
|
232
|
+
legacy.pool.links = { executiveSummary: { uri: 'ipfs://QmFactsheet', mime: 'application/pdf' } };
|
|
233
|
+
const overview = migratePoolMetadataToV2(legacy).pool.factsheet.body.find((b) => b.id === 'overview');
|
|
234
|
+
expect(overview).to.deep.equal({
|
|
235
|
+
type: 'text',
|
|
236
|
+
id: 'overview',
|
|
237
|
+
title: 'Overview',
|
|
238
|
+
body: 'A long issuer description.',
|
|
239
|
+
logo: { uri: 'ipfs://QmLogo', mime: 'image/png' },
|
|
240
|
+
links: [{ label: 'Factsheet', target: { kind: 'linkRef', linkRef: 'executiveSummary' } }],
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
it('omits the Overview logo and Factsheet link when their source fields are absent/empty', () => {
|
|
244
|
+
const legacy = legacyFixture();
|
|
245
|
+
legacy.pool.issuer.logo = { uri: '', mime: 'image/png' }; // empty uri => treated as absent
|
|
246
|
+
legacy.pool.links = { executiveSummary: null };
|
|
247
|
+
const overview = migratePoolMetadataToV2(legacy).pool.factsheet.body.find((b) => b.id === 'overview');
|
|
248
|
+
expect(overview).to.deep.equal({
|
|
249
|
+
type: 'text',
|
|
250
|
+
id: 'overview',
|
|
251
|
+
title: 'Overview',
|
|
252
|
+
body: 'A long issuer description.',
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
it('builds a Documents tile section (full-width) from rating reports, dropping unsafe URIs', () => {
|
|
133
256
|
const legacy = legacyFixture();
|
|
134
257
|
legacy.pool.poolRatings = [
|
|
135
|
-
{ agency: 'Evil
|
|
258
|
+
{ agency: 'Evil', reportFile: { uri: 'javascript:alert(1)', mime: 'text/html' } },
|
|
136
259
|
{ agency: 'Fitch', reportFile: { uri: 'https://x.io/r2', mime: 'application/pdf' } },
|
|
137
260
|
];
|
|
138
|
-
const
|
|
261
|
+
const { body, sections } = migratePoolMetadataToV2(legacy).pool.factsheet;
|
|
262
|
+
expect(body.some((b) => b.id === 'documents')).to.equal(false); // not in the header region
|
|
263
|
+
const documents = sections.find((s) => s.id === 'documents');
|
|
139
264
|
expect(documents).to.deep.equal({
|
|
140
|
-
type: '
|
|
265
|
+
type: 'documents',
|
|
141
266
|
id: 'documents',
|
|
142
267
|
title: 'Documents',
|
|
143
|
-
|
|
268
|
+
items: [
|
|
269
|
+
{
|
|
270
|
+
title: 'Fitch rating report',
|
|
271
|
+
target: { kind: 'file', file: { uri: 'https://x.io/r2', mime: 'application/pdf' } },
|
|
272
|
+
},
|
|
273
|
+
],
|
|
144
274
|
});
|
|
145
275
|
});
|
|
146
|
-
it('omits the Documents block entirely when no rating has a safe URI', () => {
|
|
276
|
+
it('omits the Documents block entirely when no rating has a safe report URI', () => {
|
|
147
277
|
const legacy = legacyFixture();
|
|
148
278
|
legacy.pool.poolRatings = [{ agency: 'X', reportFile: { uri: 'javascript:alert(1)', mime: 'text/html' } }];
|
|
149
|
-
const
|
|
150
|
-
expect(documents).to.equal(
|
|
279
|
+
const { body, sections } = migratePoolMetadataToV2(legacy).pool.factsheet;
|
|
280
|
+
expect(body.some((b) => b.id === 'documents')).to.equal(false);
|
|
281
|
+
expect((sections ?? []).some((s) => s.id === 'documents')).to.equal(false);
|
|
282
|
+
});
|
|
283
|
+
it('omits sections (app renders defaults) when there is no documents block or holdings blob', () => {
|
|
284
|
+
const legacy = legacyFixture();
|
|
285
|
+
legacy.pool.poolRatings = []; // no rating reports => no Documents section
|
|
286
|
+
expect(migratePoolMetadataToV2(legacy).pool.factsheet.sections).to.equal(undefined);
|
|
287
|
+
});
|
|
288
|
+
it('folds the legacy holdings blob into a table section, faithfully, dropping no column', () => {
|
|
289
|
+
const legacy = legacyFixture();
|
|
290
|
+
legacy.pool.poolRatings = []; // isolate the holdings section
|
|
291
|
+
legacy.holdings = {
|
|
292
|
+
headers: ['Asset', 'ISIN', 'Amount'],
|
|
293
|
+
data: [
|
|
294
|
+
{ Asset: 'T-Bill 2026', ISIN: 'US912796RW0', Amount: 1_000_000 },
|
|
295
|
+
{ Asset: 'T-Bill 2027', ISIN: null, Amount: '2500000' },
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
const v2 = migratePoolMetadataToV2(legacy);
|
|
299
|
+
expect(v2.pool.factsheet.sections).to.deep.equal([
|
|
300
|
+
{
|
|
301
|
+
type: 'table',
|
|
302
|
+
id: 'holdings',
|
|
303
|
+
title: 'Holdings',
|
|
304
|
+
headers: ['Asset', 'ISIN', 'Amount'],
|
|
305
|
+
rows: [
|
|
306
|
+
['T-Bill 2026', 'US912796RW0', 1_000_000], // number stays a number
|
|
307
|
+
['T-Bill 2027', '', '2500000'], // null -> '', numeric string stays a string
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
]);
|
|
311
|
+
// holdings is no longer a top-level field on the v2 document.
|
|
312
|
+
expect(v2).to.not.have.property('holdings');
|
|
151
313
|
});
|
|
152
|
-
it('
|
|
153
|
-
|
|
314
|
+
it('skips the holdings table when headers are empty or data is missing', () => {
|
|
315
|
+
const emptyHeaders = legacyFixture();
|
|
316
|
+
emptyHeaders.pool.poolRatings = [];
|
|
317
|
+
emptyHeaders.holdings = { headers: [], data: [{ x: 1 }] };
|
|
318
|
+
expect(migratePoolMetadataToV2(emptyHeaders).pool.factsheet.sections).to.equal(undefined);
|
|
319
|
+
const missingData = legacyFixture();
|
|
320
|
+
missingData.pool.poolRatings = [];
|
|
321
|
+
missingData.holdings = { headers: ['A'] };
|
|
322
|
+
expect(migratePoolMetadataToV2(missingData).pool.factsheet.sections).to.equal(undefined);
|
|
154
323
|
});
|
|
155
324
|
it('is idempotent and returns v2 input unchanged', () => {
|
|
156
325
|
const once = migratePoolMetadataToV2(legacyFixture());
|
|
@@ -163,6 +332,87 @@ describe('migratePoolMetadataToV2', () => {
|
|
|
163
332
|
expect(isPoolMetadataV2(v2)).to.equal(true);
|
|
164
333
|
expect(() => parsePoolMetadataV2(v2)).to.not.throw();
|
|
165
334
|
});
|
|
335
|
+
describe('intra-v2 upgrade (documents written by an earlier SDK)', () => {
|
|
336
|
+
// The shape an SDK <= 1.16.0 produced: flat keyFacts, chart series/dataRef, top-level holdings.
|
|
337
|
+
const oldV2 = () => ({
|
|
338
|
+
version: 2,
|
|
339
|
+
pool: {
|
|
340
|
+
name: 'Old V2 Pool',
|
|
341
|
+
factsheet: {
|
|
342
|
+
keyFacts: [
|
|
343
|
+
{ label: 'Issuer', value: 'Acme' },
|
|
344
|
+
{ label: 'APY', valueRef: 'apy' },
|
|
345
|
+
],
|
|
346
|
+
body: [
|
|
347
|
+
{ type: 'chart', id: 'c1', chartType: 'line', series: [{ name: 's', data: [{ x: 1, y: 2 }] }] },
|
|
348
|
+
{ type: 'chart', id: 'c2', chartType: 'donut', dataRef: 'apyVsBenchmarks' },
|
|
349
|
+
{
|
|
350
|
+
type: 'tabGroup',
|
|
351
|
+
id: 'tg',
|
|
352
|
+
tabs: [
|
|
353
|
+
{
|
|
354
|
+
label: 'T',
|
|
355
|
+
block: {
|
|
356
|
+
type: 'chart',
|
|
357
|
+
id: 'c3',
|
|
358
|
+
chartType: 'bar',
|
|
359
|
+
series: [{ name: 's', data: [{ label: 'a', value: 1 }] }],
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
shareClasses: {},
|
|
368
|
+
holdings: { headers: ['A'], data: [{ A: 'x' }] },
|
|
369
|
+
});
|
|
370
|
+
it('upgrades flat keyFacts into a single Key facts group with discriminated values', () => {
|
|
371
|
+
const { keyFacts } = migratePoolMetadataToV2(oldV2()).pool.factsheet;
|
|
372
|
+
expect(keyFacts).to.deep.equal([
|
|
373
|
+
{
|
|
374
|
+
type: 'keyFactGroup',
|
|
375
|
+
id: 'key-facts',
|
|
376
|
+
title: 'Key facts',
|
|
377
|
+
items: [
|
|
378
|
+
{ label: 'Issuer', value: { kind: 'text', text: 'Acme' } },
|
|
379
|
+
{ label: 'APY', value: { kind: 'ref', ref: 'apy' } },
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
]);
|
|
383
|
+
});
|
|
384
|
+
it('upgrades chart series/dataRef into discriminated data (incl. inside tab groups)', () => {
|
|
385
|
+
const body = migratePoolMetadataToV2(oldV2()).pool.factsheet.body;
|
|
386
|
+
const c1 = body.find((b) => b.id === 'c1');
|
|
387
|
+
expect(c1.data).to.deep.equal({ kind: 'inline', series: [{ name: 's', data: [{ x: 1, y: 2 }] }] });
|
|
388
|
+
expect(c1).to.not.have.property('series');
|
|
389
|
+
const c2 = body.find((b) => b.id === 'c2');
|
|
390
|
+
expect(c2.data).to.deep.equal({ kind: 'ref', dataRef: 'apyVsBenchmarks' });
|
|
391
|
+
const tg = body.find((b) => b.id === 'tg');
|
|
392
|
+
expect(tg.tabs[0].block.data).to.deep.equal({
|
|
393
|
+
kind: 'inline',
|
|
394
|
+
series: [{ name: 's', data: [{ label: 'a', value: 1 }] }],
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
it('folds a top-level holdings blob into a factsheet table section and removes the top-level field', () => {
|
|
398
|
+
const v2 = migratePoolMetadataToV2(oldV2());
|
|
399
|
+
expect(v2).to.not.have.property('holdings');
|
|
400
|
+
const holdings = v2.pool.factsheet.sections.find((s) => s.id === 'holdings');
|
|
401
|
+
expect(holdings).to.deep.equal({
|
|
402
|
+
type: 'table',
|
|
403
|
+
id: 'holdings',
|
|
404
|
+
title: 'Holdings',
|
|
405
|
+
headers: ['A'],
|
|
406
|
+
rows: [['x']],
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
it('produces a document that passes the current validator, and is then idempotent', () => {
|
|
410
|
+
const upgraded = migratePoolMetadataToV2(oldV2());
|
|
411
|
+
expect(() => parsePoolMetadataV2(upgraded)).to.not.throw();
|
|
412
|
+
// Already current shape -> returned unchanged (same reference).
|
|
413
|
+
expect(migratePoolMetadataToV2(upgraded)).to.equal(upgraded);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
166
416
|
});
|
|
167
417
|
describe('parsePoolMetadataV2', () => {
|
|
168
418
|
it('accepts the v2 mock exercising every block type', () => {
|
|
@@ -177,17 +427,17 @@ describe('parsePoolMetadataV2', () => {
|
|
|
177
427
|
it('rejects a missing pool name', () => {
|
|
178
428
|
expect(() => parsePoolMetadataV2({ version: 2, pool: {} })).to.throw(/pool.name/);
|
|
179
429
|
});
|
|
180
|
-
it('rejects a chart
|
|
430
|
+
it('rejects a chart whose data has an invalid kind', () => {
|
|
181
431
|
const bad = clone(mockPoolMetadataV2);
|
|
182
|
-
const chart = bad.pool.factsheet.body.find((b) => b.id === '
|
|
183
|
-
chart.
|
|
184
|
-
expect(() => parsePoolMetadataV2(bad)).to.throw(/
|
|
432
|
+
const chart = bad.pool.factsheet.body.find((b) => b.id === 'inline-chart');
|
|
433
|
+
chart.data = { kind: 'live' };
|
|
434
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/data has an invalid kind/);
|
|
185
435
|
});
|
|
186
|
-
it('rejects a chart
|
|
436
|
+
it('rejects a chart missing its data object', () => {
|
|
187
437
|
const bad = clone(mockPoolMetadataV2);
|
|
188
438
|
const chart = bad.pool.factsheet.body.find((b) => b.id === 'inline-chart');
|
|
189
|
-
delete chart.
|
|
190
|
-
expect(() => parsePoolMetadataV2(bad)).to.throw(/
|
|
439
|
+
delete chart.data;
|
|
440
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/data must be an object/);
|
|
191
441
|
});
|
|
192
442
|
it('rejects an unknown section ref (closed, SDK-enforced registry)', () => {
|
|
193
443
|
const bad = clone(mockPoolMetadataV2);
|
|
@@ -216,11 +466,11 @@ describe('parsePoolMetadataV2', () => {
|
|
|
216
466
|
bad.pool.factsheet.body[0].type = 'video';
|
|
217
467
|
expect(() => parsePoolMetadataV2(bad)).to.throw(/unknown block type/);
|
|
218
468
|
});
|
|
219
|
-
it('rejects an unknown chart dataRef (closed, SDK-enforced registry)', () => {
|
|
469
|
+
it('rejects an unknown chart data.dataRef (closed, SDK-enforced registry)', () => {
|
|
220
470
|
const bad = clone(mockPoolMetadataV2);
|
|
221
471
|
const chart = bad.pool.factsheet.body.find((b) => b.id === 'live-chart');
|
|
222
|
-
chart.
|
|
223
|
-
expect(() => parsePoolMetadataV2(bad)).to.throw(/dataRef is invalid/);
|
|
472
|
+
chart.data = { kind: 'ref', dataRef: 'someFutureDataset' };
|
|
473
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/data.dataRef is invalid/);
|
|
224
474
|
});
|
|
225
475
|
it('rejects an unknown live-table indexer metric (closed, SDK-enforced registry)', () => {
|
|
226
476
|
const bad = clone(mockPoolMetadataV2);
|
|
@@ -236,5 +486,176 @@ describe('parsePoolMetadataV2', () => {
|
|
|
236
486
|
};
|
|
237
487
|
expect(() => parsePoolMetadataV2(minimal)).to.not.throw();
|
|
238
488
|
});
|
|
489
|
+
it('rejects a flat KeyFact[] right column (groups only, no back-compat)', () => {
|
|
490
|
+
const bad = clone(mockPoolMetadataV2);
|
|
491
|
+
bad.pool.factsheet.keyFacts = [{ label: 'Issuer', value: { kind: 'text', text: 'x' } }];
|
|
492
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/keyFacts\[0\].type must be 'keyFactGroup'/);
|
|
493
|
+
});
|
|
494
|
+
it('rejects a key fact whose value has an unknown kind', () => {
|
|
495
|
+
const bad = clone(mockPoolMetadataV2);
|
|
496
|
+
bad.pool.factsheet.keyFacts[0].items[0].value = { kind: 'live' };
|
|
497
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/value has an invalid kind/);
|
|
498
|
+
});
|
|
499
|
+
it('rejects an unknown KeyFactValue ref (closed, SDK-enforced registry)', () => {
|
|
500
|
+
const bad = clone(mockPoolMetadataV2);
|
|
501
|
+
bad.pool.factsheet.keyFacts[0].items[0].value = { kind: 'ref', ref: 'someFutureRef' };
|
|
502
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/value.ref is invalid/);
|
|
503
|
+
});
|
|
504
|
+
it('accepts the known KeyFactValue refs apy and availableNetworks', () => {
|
|
505
|
+
const ok = clone(mockPoolMetadataV2);
|
|
506
|
+
ok.pool.factsheet.keyFacts[0].items[0].value = { kind: 'ref', ref: 'availableNetworks' };
|
|
507
|
+
expect(() => parsePoolMetadataV2(ok)).to.not.throw();
|
|
508
|
+
});
|
|
509
|
+
it('rejects an icons key fact with a malformed IconRef', () => {
|
|
510
|
+
const bad = clone(mockPoolMetadataV2);
|
|
511
|
+
bad.pool.factsheet.keyFacts[1].items[0].value = { kind: 'icons', icons: [{ source: 'nope' }] };
|
|
512
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/icons\[0\] has an invalid source/);
|
|
513
|
+
});
|
|
514
|
+
it('accepts both app and metadata IconRefs (app icon key is shape-validated)', () => {
|
|
515
|
+
const ok = clone(mockPoolMetadataV2);
|
|
516
|
+
ok.pool.factsheet.keyFacts[1].items[0].value = {
|
|
517
|
+
kind: 'icons',
|
|
518
|
+
icons: [
|
|
519
|
+
{ source: 'app', key: 'any-future-icon', label: 'X' },
|
|
520
|
+
{ source: 'metadata', file: { uri: 'ipfs://Qm', mime: 'image/svg+xml' } },
|
|
521
|
+
],
|
|
522
|
+
};
|
|
523
|
+
expect(() => parsePoolMetadataV2(ok)).to.not.throw();
|
|
524
|
+
});
|
|
525
|
+
it('rejects a key fact group missing items', () => {
|
|
526
|
+
const bad = clone(mockPoolMetadataV2);
|
|
527
|
+
delete bad.pool.factsheet.keyFacts[0].items;
|
|
528
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/keyFacts\[0\].items must be an array/);
|
|
529
|
+
});
|
|
530
|
+
// --- #482: geo-restrictions, documents/accordion blocks, link targets, combined visibility ---
|
|
531
|
+
it('accepts geo-restricted visibility (single + combined) and a valid geoRestrictions list', () => {
|
|
532
|
+
expect(() => parsePoolMetadataV2(mockPoolMetadataV2)).to.not.throw();
|
|
533
|
+
});
|
|
534
|
+
it('accepts geo-restricted elements even when pool.geoRestrictions is absent (nothing restricted)', () => {
|
|
535
|
+
const doc = clone(mockPoolMetadataV2);
|
|
536
|
+
delete doc.pool.geoRestrictions;
|
|
537
|
+
expect(() => parsePoolMetadataV2(doc)).to.not.throw();
|
|
538
|
+
});
|
|
539
|
+
it('rejects geoRestrictions regions that are not ISO 3166-1 alpha-2', () => {
|
|
540
|
+
const bad = clone(mockPoolMetadataV2);
|
|
541
|
+
bad.pool.geoRestrictions = { regions: ['US', 'USA'] };
|
|
542
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/geoRestrictions.regions\[1\] must be an ISO 3166-1 alpha-2 code/);
|
|
543
|
+
});
|
|
544
|
+
it('rejects an invalid visibility scalar', () => {
|
|
545
|
+
const bad = clone(mockPoolMetadataV2);
|
|
546
|
+
bad.pool.factsheet.keyFacts[0].visibility = 'continent-blocked';
|
|
547
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/visibility/);
|
|
548
|
+
});
|
|
549
|
+
it('rejects a visibility array containing a non-gate (public/hidden or unknown)', () => {
|
|
550
|
+
const bad = clone(mockPoolMetadataV2);
|
|
551
|
+
bad.pool.factsheet.keyFacts[0].visibility = ['whitelisted', 'public'];
|
|
552
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/invalid visibility gate/);
|
|
553
|
+
});
|
|
554
|
+
it('rejects a visibility array with duplicate gates', () => {
|
|
555
|
+
const bad = clone(mockPoolMetadataV2);
|
|
556
|
+
bad.pool.factsheet.keyFacts[0].visibility = ['whitelisted', 'whitelisted'];
|
|
557
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/duplicate visibility gate/);
|
|
558
|
+
});
|
|
559
|
+
it('rejects an empty visibility array', () => {
|
|
560
|
+
const bad = clone(mockPoolMetadataV2);
|
|
561
|
+
bad.pool.factsheet.keyFacts[0].visibility = [];
|
|
562
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/visibility array must not be empty/);
|
|
563
|
+
});
|
|
564
|
+
it('rejects a link target with an unknown kind', () => {
|
|
565
|
+
const bad = clone(mockPoolMetadataV2);
|
|
566
|
+
const docs = bad.pool.factsheet.body.find((b) => b.id === 'docs');
|
|
567
|
+
docs.items[0].target = { kind: 'mailto', mailto: 'x@y.z' };
|
|
568
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/target has an invalid kind/);
|
|
569
|
+
});
|
|
570
|
+
it('rejects a documents item missing its target', () => {
|
|
571
|
+
const bad = clone(mockPoolMetadataV2);
|
|
572
|
+
const docs = bad.pool.factsheet.body.find((b) => b.id === 'docs');
|
|
573
|
+
delete docs.items[0].target;
|
|
574
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/items\[0\].target must be an object/);
|
|
575
|
+
});
|
|
576
|
+
it('rejects an accordion item whose inner block is not a leaf type', () => {
|
|
577
|
+
const bad = clone(mockPoolMetadataV2);
|
|
578
|
+
const accordion = bad.pool.factsheet.body.find((b) => b.id === 'details-accordion');
|
|
579
|
+
accordion.items[0].block = { type: 'documents', id: 'nope', items: [] };
|
|
580
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/block has an invalid type/);
|
|
581
|
+
});
|
|
582
|
+
it('rejects an accordion item with a non-boolean defaultOpen', () => {
|
|
583
|
+
const bad = clone(mockPoolMetadataV2);
|
|
584
|
+
const accordion = bad.pool.factsheet.body.find((b) => b.id === 'details-accordion');
|
|
585
|
+
accordion.items[0].defaultOpen = 'yes';
|
|
586
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/defaultOpen must be a boolean/);
|
|
587
|
+
});
|
|
588
|
+
it('rejects an overview text-block link with a malformed target', () => {
|
|
589
|
+
const bad = clone(mockPoolMetadataV2);
|
|
590
|
+
const overview = bad.pool.factsheet.body.find((b) => b.id === 'overview');
|
|
591
|
+
overview.links[0].target = { kind: 'linkRef' }; // missing linkRef string
|
|
592
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/linkRef must be a string/);
|
|
593
|
+
});
|
|
594
|
+
// --- pool.links.documents (reusable named documents) ---
|
|
595
|
+
it('accepts pool.links.documents (file and href docs)', () => {
|
|
596
|
+
expect(() => parsePoolMetadataV2(mockPoolMetadataV2)).to.not.throw();
|
|
597
|
+
});
|
|
598
|
+
it('rejects a link document setting both file and href', () => {
|
|
599
|
+
const bad = clone(mockPoolMetadataV2);
|
|
600
|
+
bad.pool.links.documents = [
|
|
601
|
+
{ key: 'x', label: 'X', file: { uri: 'ipfs://Q', mime: 'application/pdf' }, href: 'https://x.io' },
|
|
602
|
+
];
|
|
603
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/exactly one of `file` or `href`/);
|
|
604
|
+
});
|
|
605
|
+
it('rejects a link document setting neither file nor href', () => {
|
|
606
|
+
const bad = clone(mockPoolMetadataV2);
|
|
607
|
+
bad.pool.links.documents = [{ key: 'x', label: 'X' }];
|
|
608
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/exactly one of `file` or `href`/);
|
|
609
|
+
});
|
|
610
|
+
it('rejects duplicate link-document keys', () => {
|
|
611
|
+
const bad = clone(mockPoolMetadataV2);
|
|
612
|
+
bad.pool.links.documents = [
|
|
613
|
+
{ key: 'dup', label: 'A', href: 'https://a.io' },
|
|
614
|
+
{ key: 'dup', label: 'B', href: 'https://b.io' },
|
|
615
|
+
];
|
|
616
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/is duplicated/);
|
|
617
|
+
});
|
|
618
|
+
it('rejects a link-document key colliding with a built-in', () => {
|
|
619
|
+
const bad = clone(mockPoolMetadataV2);
|
|
620
|
+
bad.pool.links.documents = [{ key: 'website', label: 'W', href: 'https://w.io' }];
|
|
621
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/collides with a built-in/);
|
|
622
|
+
});
|
|
623
|
+
it('rejects a link document with an empty key', () => {
|
|
624
|
+
const bad = clone(mockPoolMetadataV2);
|
|
625
|
+
bad.pool.links.documents = [{ key: '', label: 'X', href: 'https://x.io' }];
|
|
626
|
+
expect(() => parsePoolMetadataV2(bad)).to.throw(/key must be a non-empty string/);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
describe('resolveLinkTarget', () => {
|
|
630
|
+
const links = mockPoolMetadataV2.pool.links;
|
|
631
|
+
it('resolves inline file and href targets', () => {
|
|
632
|
+
expect(resolveLinkTarget(links, { kind: 'file', file: { uri: 'ipfs://Q', mime: 'application/pdf' } })).to.deep.equal({
|
|
633
|
+
file: { uri: 'ipfs://Q', mime: 'application/pdf' },
|
|
634
|
+
});
|
|
635
|
+
expect(resolveLinkTarget(links, { kind: 'href', href: 'https://x.io' })).to.deep.equal({ href: 'https://x.io' });
|
|
636
|
+
});
|
|
637
|
+
it('resolves built-in linkRefs (executiveSummary -> file, website/forum -> url)', () => {
|
|
638
|
+
expect(resolveLinkTarget(links, { kind: 'linkRef', linkRef: 'executiveSummary' })).to.deep.equal({
|
|
639
|
+
file: links.executiveSummary,
|
|
640
|
+
});
|
|
641
|
+
expect(resolveLinkTarget(links, { kind: 'linkRef', linkRef: 'website' })).to.deep.equal({
|
|
642
|
+
href: 'https://newsilver.com',
|
|
643
|
+
});
|
|
644
|
+
expect(resolveLinkTarget(links, { kind: 'linkRef', linkRef: 'forum' })).to.deep.equal({
|
|
645
|
+
href: 'https://gov.centrifuge.io/tag/newsilver',
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
it('resolves named documents by key (file or href)', () => {
|
|
649
|
+
expect(resolveLinkTarget(links, { kind: 'linkRef', linkRef: 'prospectus' })).to.deep.equal({
|
|
650
|
+
file: { uri: 'ipfs://QmProspectusDoc', mime: 'application/pdf' },
|
|
651
|
+
});
|
|
652
|
+
expect(resolveLinkTarget(links, { kind: 'linkRef', linkRef: 'termsOfService' })).to.deep.equal({
|
|
653
|
+
href: 'https://example.com/tos',
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
it('returns null for an unknown linkRef or missing links', () => {
|
|
657
|
+
expect(resolveLinkTarget(links, { kind: 'linkRef', linkRef: 'nope' })).to.equal(null);
|
|
658
|
+
expect(resolveLinkTarget(undefined, { kind: 'linkRef', linkRef: 'website' })).to.equal(null);
|
|
659
|
+
});
|
|
239
660
|
});
|
|
240
661
|
//# sourceMappingURL=poolMetadataMigration.test.js.map
|