mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Wire Premium 2.0 private copy unlock path
This commit is contained in:
1 parent
92c41442dc
commit
170756f722
2 files changed
+166
-14
No files matched your search
+125
-14
@@ -3921,6 +3921,10 @@
|
|||||||
cp noble-bundle.min.js <repo>/src/
|
cp noble-bundle.min.js <repo>/src/
|
||||||
-->
|
-->
|
||||||
<script src="noble-bundle.min.js"></script>
|
<script src="noble-bundle.min.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import * as Premium2 from './premium2-helpers.mjs';
|
||||||
|
window.ArtstrPremium2 = Premium2;
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="nostrWidget" id="nostrWidget">
|
<div class="nostrWidget" id="nostrWidget">
|
||||||
@@ -7685,17 +7689,19 @@
|
|||||||
let vaultErr = null;
|
let vaultErr = null;
|
||||||
if (row.premium && decryptedPayload) {
|
if (row.premium && decryptedPayload) {
|
||||||
if (statusToast?.dismiss) statusToast.dismiss();
|
if (statusToast?.dismiss) statusToast.dismiss();
|
||||||
statusToast = toast('Unlocked. Syncing to your purchase vault…', { duration: 30000 });
|
statusToast = toast('Unlocked. Saving your private purchased copy…', { duration: 30000 });
|
||||||
const vaultItem = _buildVaultItemForUnlock({
|
|
||||||
row,
|
|
||||||
totalSats: min,
|
|
||||||
creatorSats: result.creatorSats,
|
|
||||||
platformSats: result.platformSats,
|
|
||||||
creatorPreimage: result.creatorPreimage,
|
|
||||||
platformPreimage: result.platformPreimage,
|
|
||||||
payload: decryptedPayload,
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
|
const vaultItem = await _publishPurchaseCopyAndBuildVaultItem({
|
||||||
|
row,
|
||||||
|
totalSats: min,
|
||||||
|
creatorSats: result.creatorSats,
|
||||||
|
platformSats: result.platformSats,
|
||||||
|
creatorPreimage: result.creatorPreimage,
|
||||||
|
platformPreimage: result.platformPreimage,
|
||||||
|
payload: decryptedPayload,
|
||||||
|
});
|
||||||
|
if (statusToast?.dismiss) statusToast.dismiss();
|
||||||
|
statusToast = toast('Private copy saved. Syncing purchase vault…', { duration: 30000 });
|
||||||
await upsertPurchaseVaultItem(vaultItem);
|
await upsertPurchaseVaultItem(vaultItem);
|
||||||
vaultSynced = true;
|
vaultSynced = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -7824,13 +7830,20 @@
|
|||||||
// fields — the per-item event carries those.
|
// fields — the per-item event carries those.
|
||||||
function _vaultItemStub(item) {
|
function _vaultItemStub(item) {
|
||||||
return {
|
return {
|
||||||
|
schema: item.schema,
|
||||||
a: item.a,
|
a: item.a,
|
||||||
eventId: item.eventId,
|
eventId: item.eventId,
|
||||||
|
privateCopy: item.privateCopy,
|
||||||
|
privateCopyEventId: item.privateCopyEventId,
|
||||||
creatorPubkey: item.creatorPubkey,
|
creatorPubkey: item.creatorPubkey,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
mode: item.mode,
|
mode: item.mode,
|
||||||
unlockedAt: item.unlockedAt,
|
unlockedAt: item.unlockedAt,
|
||||||
amountSats: item.amountSats,
|
amountSats: item.amountSats,
|
||||||
|
claimEpoch: item.claimEpoch,
|
||||||
|
softgateEpoch: item.softgateEpoch,
|
||||||
|
licenseType: item.licenseType,
|
||||||
|
receiptSummary: item.receiptSummary,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8013,6 +8026,93 @@
|
|||||||
return await _publishVault(next);
|
return await _publishVault(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _parseAddressCoord(coord) {
|
||||||
|
if (typeof coord !== 'string') return null;
|
||||||
|
const parts = coord.split(':');
|
||||||
|
if (parts.length < 3) return null;
|
||||||
|
const kind = Number(parts[0]);
|
||||||
|
const pubkey = parts[1] || '';
|
||||||
|
const dTag = parts.slice(2).join(':');
|
||||||
|
if (!Number.isFinite(kind) || !pubkey || !dTag) return null;
|
||||||
|
return { kind, pubkey, dTag };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _loadPurchaseCopyPayload(privateCopyCoord) {
|
||||||
|
const parsed = _parseAddressCoord(privateCopyCoord);
|
||||||
|
if (!parsed) throw new Error('Purchase copy pointer is malformed.');
|
||||||
|
const events = await queryRelays(_vaultRelayUnion(), {
|
||||||
|
kinds: [parsed.kind],
|
||||||
|
authors: [parsed.pubkey],
|
||||||
|
'#d': [parsed.dTag],
|
||||||
|
limit: 5,
|
||||||
|
}, { timeoutMs: 4500 });
|
||||||
|
const latest = (events || []).filter((e) => e?.content)
|
||||||
|
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0];
|
||||||
|
if (!latest) throw new Error('Private purchase copy was not found on relays.');
|
||||||
|
const plaintext = await _selfDecrypt(latest.content);
|
||||||
|
const content = JSON.parse(plaintext);
|
||||||
|
if (content?.kind !== 'artstr-purchase-copy' || !content?.payload) {
|
||||||
|
throw new Error('Private purchase copy content was not valid.');
|
||||||
|
}
|
||||||
|
return { copy: content, event: latest };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _publishPurchaseCopyAndBuildVaultItem({ row, totalSats, creatorSats, platformSats, creatorPreimage, platformPreimage, payload }) {
|
||||||
|
const helpers = window.ArtstrPremium2;
|
||||||
|
if (!helpers?.buildPurchaseCopyPlaintext || !helpers?.buildPurchaseCopyEventDraft || !helpers?.buildVaultV2Item || !helpers?.buildPurchaseCopyCoordinate) {
|
||||||
|
throw new Error('Premium 2.0 helper module did not load.');
|
||||||
|
}
|
||||||
|
const source = {
|
||||||
|
a: row.premium?.address || '',
|
||||||
|
eventId: row.e?.id || '',
|
||||||
|
creatorPubkey: row.e?.pubkey || '',
|
||||||
|
title: row.title || '',
|
||||||
|
premiumMode: row.premium?.envelope?.premiumMode || premiumEventTagValue(row.e, 'premium-mode') || 'softgate-v1',
|
||||||
|
softgateEpoch: row.premium?.envelope?.softgate?.epoch || premiumEventTagValue(row.e, 'softgate-epoch') || '',
|
||||||
|
claimEpoch: row.premium?.envelope?.softgate?.claimEpoch || premiumEventTagValue(row.e, 'claim-epoch') || '',
|
||||||
|
};
|
||||||
|
const purchase = {
|
||||||
|
buyerPubkey: state.community?.myPubkey || '',
|
||||||
|
amountSats: totalSats,
|
||||||
|
unlockedAt: Math.floor(Date.now() / 1000),
|
||||||
|
receipts: [
|
||||||
|
{ role: 'creator', recipientPubkey: row.e?.pubkey || '', amountSats: creatorSats, preimage: creatorPreimage || '' },
|
||||||
|
{ role: 'platform', recipientPubkey: platform.getPubkeyHex(), amountSats: platformSats, preimage: platformPreimage || '' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const license = {
|
||||||
|
type: row.premium?.envelope?.license?.type || 'personal-use-template',
|
||||||
|
snapshot: true,
|
||||||
|
sourceCanChange: true,
|
||||||
|
};
|
||||||
|
const copyPlaintext = helpers.buildPurchaseCopyPlaintext({
|
||||||
|
source,
|
||||||
|
purchase,
|
||||||
|
license,
|
||||||
|
payload,
|
||||||
|
payloadSchemaVersion: row.premium?.envelope?.payloadSchemaVersion || payload?.version || 5,
|
||||||
|
});
|
||||||
|
const encryptedContent = await _selfEncrypt(JSON.stringify(copyPlaintext));
|
||||||
|
const copyDraft = await helpers.buildPurchaseCopyEventDraft({
|
||||||
|
sourceAddress: source.a,
|
||||||
|
encryptedContent,
|
||||||
|
});
|
||||||
|
const signedCopy = await signNostrEvent(copyDraft);
|
||||||
|
await publishToRelays(signedCopy, _vaultRelayUnion());
|
||||||
|
const privateCopy = await helpers.buildPurchaseCopyCoordinate({
|
||||||
|
buyerPubkey: state.community?.myPubkey || '',
|
||||||
|
sourceAddress: source.a,
|
||||||
|
});
|
||||||
|
return helpers.buildVaultV2Item({
|
||||||
|
source,
|
||||||
|
privateCopy,
|
||||||
|
privateCopyEventId: signedCopy.id || '',
|
||||||
|
purchase,
|
||||||
|
mode: row.mode || payload?.templateMode || '',
|
||||||
|
licenseType: license.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function _buildVaultItemForUnlock({ row, totalSats, creatorSats, platformSats, creatorPreimage, platformPreimage, payload }) {
|
function _buildVaultItemForUnlock({ row, totalSats, creatorSats, platformSats, creatorPreimage, platformPreimage, payload }) {
|
||||||
return {
|
return {
|
||||||
a: row.premium?.address || '',
|
a: row.premium?.address || '',
|
||||||
@@ -8044,15 +8144,26 @@
|
|||||||
let unavailable = 0;
|
let unavailable = 0;
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
if (!it?.eventId) continue;
|
if (!it?.eventId) continue;
|
||||||
if (!it?.payload) { unavailable++; continue; }
|
let payload = it.payload || null;
|
||||||
|
let previewJpegDataUrl = it.previewJpegDataUrl || undefined;
|
||||||
|
if (!payload && it.privateCopy) {
|
||||||
|
try {
|
||||||
|
const restored = await _loadPurchaseCopyPayload(it.privateCopy);
|
||||||
|
payload = restored.copy?.payload || null;
|
||||||
|
previewJpegDataUrl = restored.copy?.previewJpegDataUrl || previewJpegDataUrl;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[vault] private purchase-copy restore failed', it.privateCopy, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!payload) { unavailable++; continue; }
|
||||||
if (isUnlockedLocally(it.eventId, it.a)) continue;
|
if (isUnlockedLocally(it.eventId, it.a)) continue;
|
||||||
// Store under the LATEST event id from the vault, but include
|
// Store under the LATEST event id from the vault, but include
|
||||||
// the address so a future edit-in-place still picks this up.
|
// the address so a future edit-in-place still picks this up.
|
||||||
markUnlocked(it.eventId, {
|
markUnlocked(it.eventId, {
|
||||||
source: 'vault',
|
source: it.privateCopy ? 'vault-private-copy' : 'vault',
|
||||||
address: it.a || '',
|
address: it.a || '',
|
||||||
payload: it.payload || undefined,
|
payload,
|
||||||
previewJpegDataUrl: it.previewJpegDataUrl || undefined,
|
previewJpegDataUrl,
|
||||||
amountSats: it.amountSats,
|
amountSats: it.amountSats,
|
||||||
unlockedAt: it.unlockedAt,
|
unlockedAt: it.unlockedAt,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const indexPath = join(__dirname, '..', '..', 'src', 'index.html');
|
||||||
|
|
||||||
|
async function indexHtml() {
|
||||||
|
return readFile(indexPath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('index page wires Premium 2.0 private-copy unlock flow', async () => {
|
||||||
|
const html = await indexHtml();
|
||||||
|
|
||||||
|
assert.match(html, /import \* as Premium2 from '\.\/premium2-helpers\.mjs';/);
|
||||||
|
assert.match(html, /window\.ArtstrPremium2 = Premium2;/);
|
||||||
|
assert.match(html, /async function _publishPurchaseCopyAndBuildVaultItem/);
|
||||||
|
assert.match(html, /helpers\.buildPurchaseCopyPlaintext/);
|
||||||
|
assert.match(html, /helpers\.buildPurchaseCopyEventDraft/);
|
||||||
|
assert.match(html, /helpers\.buildVaultV2Item/);
|
||||||
|
assert.match(html, /await _loadPurchaseCopyPayload\(it\.privateCopy\)/);
|
||||||
|
assert.match(html, /source: it\.privateCopy \? 'vault-private-copy' : 'vault'/);
|
||||||
|
assert.match(html, /Unlocked\. Saving your private purchased copy/);
|
||||||
|
assert.match(html, /Private copy saved\. Syncing purchase vault/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('vault item stubs preserve Premium 2.0 private-copy fields', async () => {
|
||||||
|
const html = await indexHtml();
|
||||||
|
const stubStart = html.indexOf('function _vaultItemStub(item)');
|
||||||
|
const stubEnd = html.indexOf('async function _publishVaultEvent', stubStart);
|
||||||
|
const stubBody = html.slice(stubStart, stubEnd);
|
||||||
|
|
||||||
|
assert.ok(stubStart > -1, '_vaultItemStub should exist');
|
||||||
|
assert.ok(stubEnd > stubStart, '_publishVaultEvent should follow _vaultItemStub');
|
||||||
|
assert.match(stubBody, /privateCopy: item\.privateCopy/);
|
||||||
|
assert.match(stubBody, /privateCopyEventId: item\.privateCopyEventId/);
|
||||||
|
assert.match(stubBody, /claimEpoch: item\.claimEpoch/);
|
||||||
|
assert.match(stubBody, /receiptSummary: item\.receiptSummary/);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user