@solana-mobile/dapp-store-publishing-tools 0.16.0 → 1.0.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.
Files changed (53) hide show
  1. package/lib/CoreUtils.js +0 -3
  2. package/lib/index.js +1 -0
  3. package/lib/portal/attestation.js +189 -0
  4. package/lib/portal/compat.js +3 -0
  5. package/lib/portal/index.js +5 -0
  6. package/lib/portal/signer.js +432 -0
  7. package/lib/portal/types.js +1 -0
  8. package/lib/portal/workflow/contracts.js +1 -0
  9. package/lib/portal/workflow/execution.js +493 -0
  10. package/lib/portal/workflow/ingestion.js +265 -0
  11. package/lib/portal/workflow/lifecycle.js +616 -0
  12. package/lib/portal/workflow/logging.js +8 -0
  13. package/lib/portal/workflow/source/files.js +304 -0
  14. package/lib/portal/workflow/source/preparation.js +318 -0
  15. package/lib/portal/workflow/state/bundle.js +260 -0
  16. package/lib/portal/workflow/state/checkpoints.js +53 -0
  17. package/lib/portal/workflow/state/session.js +100 -0
  18. package/lib/portal/workflow.js +1 -0
  19. package/lib/publish/PublishCoreAttestation.js +18 -17
  20. package/lib/publish/PublishCoreRemove.js +7 -89
  21. package/lib/publish/PublishCoreSubmit.js +7 -117
  22. package/lib/publish/PublishCoreSupport.js +7 -86
  23. package/lib/publish/PublishCoreUpdate.js +7 -117
  24. package/lib/publish/index.js +1 -0
  25. package/lib/schemas/releaseJsonMetadata.json +1 -2
  26. package/package.json +2 -4
  27. package/src/CoreUtils.ts +0 -6
  28. package/src/index.ts +1 -0
  29. package/src/portal/attestation.ts +76 -0
  30. package/src/portal/compat.ts +5 -0
  31. package/src/portal/index.ts +5 -0
  32. package/src/portal/signer.ts +327 -0
  33. package/src/portal/types.ts +447 -0
  34. package/src/portal/workflow/contracts.ts +108 -0
  35. package/src/portal/workflow/execution.ts +412 -0
  36. package/src/portal/workflow/ingestion.ts +187 -0
  37. package/src/portal/workflow/lifecycle.ts +435 -0
  38. package/src/portal/workflow/logging.ts +17 -0
  39. package/src/portal/workflow/source/files.ts +49 -0
  40. package/src/portal/workflow/source/preparation.ts +189 -0
  41. package/src/portal/workflow/state/bundle.ts +193 -0
  42. package/src/portal/workflow/state/checkpoints.ts +70 -0
  43. package/src/portal/workflow/state/session.ts +87 -0
  44. package/src/portal/workflow.ts +9 -0
  45. package/src/publish/PublishCoreAttestation.ts +21 -26
  46. package/src/publish/PublishCoreRemove.ts +13 -109
  47. package/src/publish/PublishCoreSubmit.ts +18 -150
  48. package/src/publish/PublishCoreSupport.ts +13 -102
  49. package/src/publish/PublishCoreUpdate.ts +17 -155
  50. package/src/publish/index.ts +2 -1
  51. package/src/schemas/releaseJsonMetadata.json +1 -2
  52. package/lib/publish/dapp_publisher_portal.js +0 -206
  53. package/src/publish/dapp_publisher_portal.ts +0 -81
@@ -0,0 +1,327 @@
1
+ import { createPublicKey, verify } from "node:crypto";
2
+
3
+ import {
4
+ CreateStruct,
5
+ createInstructionDiscriminator,
6
+ isCreateArgsV1,
7
+ } from "@metaplex-foundation/mpl-token-metadata";
8
+ import { ComputeBudgetProgram, PublicKey, Transaction } from "@solana/web3.js";
9
+
10
+ import type { PublicationSigner } from "./types.js";
11
+
12
+ const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
13
+ "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
14
+ );
15
+ const TOKEN_METADATA_SEED = Buffer.from("metadata");
16
+ const TOKEN_METADATA_EDITION_SEED = Buffer.from("edition");
17
+ const ALLOWED_PUBLICATION_PROGRAM_IDS = new Set([
18
+ ComputeBudgetProgram.programId.toBase58(),
19
+ TOKEN_METADATA_PROGRAM_ID.toBase58(),
20
+ ]);
21
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
22
+
23
+ export type PublicationSignerAdapter = {
24
+ publicKey: string;
25
+ signTransaction(transaction: Transaction): Promise<Transaction>;
26
+ signMessage(message: Uint8Array): Promise<Uint8Array>;
27
+ };
28
+
29
+ export type PublicationTransactionValidation =
30
+ | {
31
+ kind: "release-mint";
32
+ expectedBlockhash: string;
33
+ expectedFeePayerAddress: string;
34
+ expectedSignerAddress: string;
35
+ expectedMintAddress: string;
36
+ expectedAppMintAddress: string;
37
+ }
38
+ | {
39
+ kind: "verify-collection";
40
+ expectedBlockhash: string;
41
+ expectedFeePayerAddress: string;
42
+ expectedSignerAddress: string;
43
+ expectedNftMintAddress: string;
44
+ expectedCollectionMintAddress: string;
45
+ expectedCollectionAuthority: string;
46
+ };
47
+
48
+ export const createPublicationSigner = (
49
+ adapter: PublicationSignerAdapter
50
+ ): PublicationSigner => ({
51
+ publicKey: adapter.publicKey,
52
+ signTransaction: adapter.signTransaction,
53
+ signMessage: adapter.signMessage,
54
+ });
55
+
56
+ export const isPublicationSigner = (
57
+ value: unknown
58
+ ): value is PublicationSigner =>
59
+ typeof value === "object" &&
60
+ value !== null &&
61
+ "publicKey" in value &&
62
+ "signTransaction" in value &&
63
+ "signMessage" in value;
64
+
65
+ function assertExactAddressSet(
66
+ actual: string[],
67
+ expected: string[],
68
+ label: string
69
+ ): void {
70
+ const normalizedActual = [...new Set(actual)].sort();
71
+ const normalizedExpected = [...new Set(expected)].sort();
72
+
73
+ if (
74
+ normalizedActual.length !== normalizedExpected.length ||
75
+ normalizedActual.some((value, index) => value !== normalizedExpected[index])
76
+ ) {
77
+ throw new Error(
78
+ `${label} signer set mismatch. Expected ${normalizedExpected.join(
79
+ ", "
80
+ )}; received ${normalizedActual.join(", ") || "[none]"}.`
81
+ );
82
+ }
83
+ }
84
+
85
+ function assertAccountsPresent(
86
+ accountAddresses: string[],
87
+ expectedAddresses: Array<[string, string]>
88
+ ): void {
89
+ for (const [label, address] of expectedAddresses) {
90
+ if (!accountAddresses.includes(address)) {
91
+ throw new Error(
92
+ `Portal transaction is missing the expected ${label} account: ${address}`
93
+ );
94
+ }
95
+ }
96
+ }
97
+
98
+ function assertExistingSignaturesValid(transaction: Transaction): void {
99
+ const signedEntries = transaction.signatures.filter(
100
+ ({ signature }) => signature !== null
101
+ );
102
+
103
+ if (signedEntries.length === 0) {
104
+ return;
105
+ }
106
+
107
+ const message = transaction.serializeMessage();
108
+ const invalidSigners = signedEntries
109
+ .filter(
110
+ ({ publicKey, signature }) =>
111
+ !verify(
112
+ null,
113
+ message,
114
+ createPublicKey({
115
+ key: Buffer.concat([
116
+ ED25519_SPKI_PREFIX,
117
+ Buffer.from(publicKey.toBytes()),
118
+ ]),
119
+ format: "der",
120
+ type: "spki",
121
+ }),
122
+ signature!
123
+ )
124
+ )
125
+ .map(({ publicKey }) => publicKey.toBase58());
126
+
127
+ if (invalidSigners.length > 0) {
128
+ throw new Error(
129
+ `Portal transaction contains invalid existing signatures for ${invalidSigners.join(
130
+ ", "
131
+ )} and may have been modified in transit.`
132
+ );
133
+ }
134
+ }
135
+
136
+ function getTokenMetadataCreateCollectionAddress(
137
+ transaction: Transaction
138
+ ): string | null {
139
+ for (const instruction of transaction.instructions) {
140
+ if (!instruction.programId.equals(TOKEN_METADATA_PROGRAM_ID)) {
141
+ continue;
142
+ }
143
+
144
+ if (
145
+ instruction.data.length === 0 ||
146
+ instruction.data[0] !== createInstructionDiscriminator
147
+ ) {
148
+ continue;
149
+ }
150
+
151
+ try {
152
+ const [decodedInstruction] = CreateStruct.deserialize(instruction.data);
153
+
154
+ if (!isCreateArgsV1(decodedInstruction.createArgs)) {
155
+ continue;
156
+ }
157
+
158
+ return decodedInstruction.createArgs.assetData.collection?.key.toBase58() ?? null;
159
+ } catch (error) {
160
+ throw new Error(
161
+ "Portal transaction contains an invalid token metadata create instruction."
162
+ );
163
+ }
164
+ }
165
+
166
+ return null;
167
+ }
168
+
169
+ function getMetadataPdaAddress(mintAddress: string): string {
170
+ return PublicKey.findProgramAddressSync(
171
+ [
172
+ TOKEN_METADATA_SEED,
173
+ TOKEN_METADATA_PROGRAM_ID.toBuffer(),
174
+ new PublicKey(mintAddress).toBuffer(),
175
+ ],
176
+ TOKEN_METADATA_PROGRAM_ID
177
+ )[0].toBase58();
178
+ }
179
+
180
+ function getMasterEditionPdaAddress(mintAddress: string): string {
181
+ return PublicKey.findProgramAddressSync(
182
+ [
183
+ TOKEN_METADATA_SEED,
184
+ TOKEN_METADATA_PROGRAM_ID.toBuffer(),
185
+ new PublicKey(mintAddress).toBuffer(),
186
+ TOKEN_METADATA_EDITION_SEED,
187
+ ],
188
+ TOKEN_METADATA_PROGRAM_ID
189
+ )[0].toBase58();
190
+ }
191
+
192
+ function validatePublicationTransaction(
193
+ signer: PublicationSigner,
194
+ transaction: Transaction,
195
+ validation: PublicationTransactionValidation
196
+ ): void {
197
+ if (transaction.recentBlockhash !== validation.expectedBlockhash) {
198
+ throw new Error(
199
+ `Portal transaction blockhash mismatch. Expected ${validation.expectedBlockhash}; received ${transaction.recentBlockhash}.`
200
+ );
201
+ }
202
+
203
+ const feePayerAddress = transaction.feePayer?.toBase58();
204
+ if (!feePayerAddress) {
205
+ throw new Error("Portal transaction is missing a fee payer.");
206
+ }
207
+ if (feePayerAddress !== validation.expectedFeePayerAddress) {
208
+ throw new Error(
209
+ `Portal transaction fee payer mismatch. Expected ${validation.expectedFeePayerAddress}; received ${feePayerAddress}.`
210
+ );
211
+ }
212
+
213
+ if (signer.publicKey !== validation.expectedSignerAddress) {
214
+ throw new Error(
215
+ `Publication signer mismatch. Expected ${validation.expectedSignerAddress}; received ${signer.publicKey}.`
216
+ );
217
+ }
218
+
219
+ assertExistingSignaturesValid(transaction);
220
+
221
+ const compiledMessage = transaction.compileMessage();
222
+ const accountAddresses = compiledMessage.accountKeys.map((key) =>
223
+ key.toBase58()
224
+ );
225
+ const requiredSignerAddresses = compiledMessage.accountKeys
226
+ .slice(0, compiledMessage.header.numRequiredSignatures)
227
+ .map((key) => key.toBase58());
228
+
229
+ if (!requiredSignerAddresses.includes(signer.publicKey)) {
230
+ throw new Error(
231
+ `Portal transaction does not require the local signer ${signer.publicKey}.`
232
+ );
233
+ }
234
+
235
+ const unexpectedPrograms = transaction.instructions
236
+ .map((instruction) => instruction.programId.toBase58())
237
+ .filter((programId) => !ALLOWED_PUBLICATION_PROGRAM_IDS.has(programId));
238
+ if (unexpectedPrograms.length > 0) {
239
+ throw new Error(
240
+ `Portal transaction includes unexpected program ids: ${[
241
+ ...new Set(unexpectedPrograms),
242
+ ].join(", ")}`
243
+ );
244
+ }
245
+
246
+ if (
247
+ !transaction.instructions.some((instruction) =>
248
+ instruction.programId.equals(TOKEN_METADATA_PROGRAM_ID)
249
+ )
250
+ ) {
251
+ throw new Error(
252
+ "Portal transaction is missing the expected token metadata instruction."
253
+ );
254
+ }
255
+
256
+ if (validation.kind === "release-mint") {
257
+ assertExactAddressSet(
258
+ requiredSignerAddresses,
259
+ [validation.expectedFeePayerAddress, validation.expectedMintAddress],
260
+ "Release mint transaction"
261
+ );
262
+ assertAccountsPresent(accountAddresses, [
263
+ ["release mint", validation.expectedMintAddress],
264
+ ]);
265
+
266
+ const actualCollectionMintAddress =
267
+ getTokenMetadataCreateCollectionAddress(transaction);
268
+ if (actualCollectionMintAddress !== validation.expectedAppMintAddress) {
269
+ throw new Error(
270
+ `Portal transaction token metadata collection mismatch. Expected ${validation.expectedAppMintAddress}; received ${actualCollectionMintAddress ?? "[none]"}.`
271
+ );
272
+ }
273
+
274
+ const mintSignature = transaction.signatures.find(({ publicKey }) =>
275
+ publicKey.equals(new PublicKey(validation.expectedMintAddress))
276
+ )?.signature;
277
+ if (!mintSignature) {
278
+ throw new Error(
279
+ "Release mint transaction is missing the pre-signed mint signature."
280
+ );
281
+ }
282
+
283
+ return;
284
+ }
285
+
286
+ assertExactAddressSet(
287
+ requiredSignerAddresses,
288
+ [
289
+ validation.expectedFeePayerAddress,
290
+ validation.expectedCollectionAuthority,
291
+ ],
292
+ "Collection verification transaction"
293
+ );
294
+ assertAccountsPresent(accountAddresses, [
295
+ [
296
+ "release metadata",
297
+ getMetadataPdaAddress(validation.expectedNftMintAddress),
298
+ ],
299
+ ["app collection mint", validation.expectedCollectionMintAddress],
300
+ [
301
+ "app collection metadata",
302
+ getMetadataPdaAddress(validation.expectedCollectionMintAddress),
303
+ ],
304
+ [
305
+ "app collection master edition",
306
+ getMasterEditionPdaAddress(validation.expectedCollectionMintAddress),
307
+ ],
308
+ ["collection authority", validation.expectedCollectionAuthority],
309
+ ]);
310
+ }
311
+
312
+ export const signSerializedTransaction = async (
313
+ signer: PublicationSigner,
314
+ serializedTransaction: string,
315
+ validation?: PublicationTransactionValidation
316
+ ): Promise<string> => {
317
+ const transaction = Transaction.from(
318
+ Buffer.from(serializedTransaction, "base64")
319
+ );
320
+
321
+ if (validation) {
322
+ validatePublicationTransaction(signer, transaction, validation);
323
+ }
324
+
325
+ const signedTransaction = await signer.signTransaction(transaction);
326
+ return signedTransaction.serialize().toString("base64");
327
+ };