@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.
- package/lib/CoreUtils.js +0 -3
- package/lib/index.js +1 -0
- package/lib/portal/attestation.js +189 -0
- package/lib/portal/compat.js +3 -0
- package/lib/portal/index.js +5 -0
- package/lib/portal/signer.js +432 -0
- package/lib/portal/types.js +1 -0
- package/lib/portal/workflow/contracts.js +1 -0
- package/lib/portal/workflow/execution.js +493 -0
- package/lib/portal/workflow/ingestion.js +265 -0
- package/lib/portal/workflow/lifecycle.js +616 -0
- package/lib/portal/workflow/logging.js +8 -0
- package/lib/portal/workflow/source/files.js +304 -0
- package/lib/portal/workflow/source/preparation.js +318 -0
- package/lib/portal/workflow/state/bundle.js +260 -0
- package/lib/portal/workflow/state/checkpoints.js +53 -0
- package/lib/portal/workflow/state/session.js +100 -0
- package/lib/portal/workflow.js +1 -0
- package/lib/publish/PublishCoreAttestation.js +18 -17
- package/lib/publish/PublishCoreRemove.js +7 -89
- package/lib/publish/PublishCoreSubmit.js +7 -117
- package/lib/publish/PublishCoreSupport.js +7 -86
- package/lib/publish/PublishCoreUpdate.js +7 -117
- package/lib/publish/index.js +1 -0
- package/lib/schemas/releaseJsonMetadata.json +1 -2
- package/package.json +2 -4
- package/src/CoreUtils.ts +0 -6
- package/src/index.ts +1 -0
- package/src/portal/attestation.ts +76 -0
- package/src/portal/compat.ts +5 -0
- package/src/portal/index.ts +5 -0
- package/src/portal/signer.ts +327 -0
- package/src/portal/types.ts +447 -0
- package/src/portal/workflow/contracts.ts +108 -0
- package/src/portal/workflow/execution.ts +412 -0
- package/src/portal/workflow/ingestion.ts +187 -0
- package/src/portal/workflow/lifecycle.ts +435 -0
- package/src/portal/workflow/logging.ts +17 -0
- package/src/portal/workflow/source/files.ts +49 -0
- package/src/portal/workflow/source/preparation.ts +189 -0
- package/src/portal/workflow/state/bundle.ts +193 -0
- package/src/portal/workflow/state/checkpoints.ts +70 -0
- package/src/portal/workflow/state/session.ts +87 -0
- package/src/portal/workflow.ts +9 -0
- package/src/publish/PublishCoreAttestation.ts +21 -26
- package/src/publish/PublishCoreRemove.ts +13 -109
- package/src/publish/PublishCoreSubmit.ts +18 -150
- package/src/publish/PublishCoreSupport.ts +13 -102
- package/src/publish/PublishCoreUpdate.ts +17 -155
- package/src/publish/index.ts +2 -1
- package/src/schemas/releaseJsonMetadata.json +1 -2
- package/lib/publish/dapp_publisher_portal.js +0 -206
- 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
|
+
};
|