mpp-test-sdk 1.0.0 → 1.1.1
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/README.md +46 -32
- package/dist/index.d.mts +92 -37
- package/dist/index.d.ts +92 -37
- package/dist/index.js +273 -85
- package/dist/index.mjs +285 -85
- package/package.json +15 -7
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
MppError: () => MppError,
|
|
24
24
|
MppFaucetError: () => MppFaucetError,
|
|
25
|
+
MppNetworkError: () => MppNetworkError,
|
|
25
26
|
MppPaymentError: () => MppPaymentError,
|
|
26
27
|
MppTimeoutError: () => MppTimeoutError,
|
|
27
28
|
createTestClient: () => createTestClient,
|
|
@@ -31,8 +32,7 @@ __export(index_exports, {
|
|
|
31
32
|
module.exports = __toCommonJS(index_exports);
|
|
32
33
|
|
|
33
34
|
// src/client.ts
|
|
34
|
-
var
|
|
35
|
-
var import_client = require("mppx/client");
|
|
35
|
+
var import_web3 = require("@solana/web3.js");
|
|
36
36
|
|
|
37
37
|
// src/errors.ts
|
|
38
38
|
var MppError = class extends Error {
|
|
@@ -44,7 +44,9 @@ var MppError = class extends Error {
|
|
|
44
44
|
var MppFaucetError = class extends MppError {
|
|
45
45
|
address;
|
|
46
46
|
constructor(address, cause) {
|
|
47
|
-
super(
|
|
47
|
+
super(
|
|
48
|
+
`Failed to airdrop SOL to wallet ${address}. The devnet/testnet faucet may be rate-limited. Wait 30s and retry, or pass a pre-funded secretKey to skip airdrop.`
|
|
49
|
+
);
|
|
48
50
|
this.name = "MppFaucetError";
|
|
49
51
|
this.address = address;
|
|
50
52
|
this.cause = cause;
|
|
@@ -65,95 +67,201 @@ var MppTimeoutError = class extends MppError {
|
|
|
65
67
|
url;
|
|
66
68
|
timeoutMs;
|
|
67
69
|
constructor(url, timeoutMs) {
|
|
68
|
-
super(`Request to ${url} timed out after ${timeoutMs}ms
|
|
70
|
+
super(`Request to ${url} timed out after ${timeoutMs}ms. Increase the timeout option or check your Solana RPC connection.`);
|
|
69
71
|
this.name = "MppTimeoutError";
|
|
70
72
|
this.url = url;
|
|
71
73
|
this.timeoutMs = timeoutMs;
|
|
72
74
|
}
|
|
73
75
|
};
|
|
76
|
+
var MppNetworkError = class extends MppError {
|
|
77
|
+
network;
|
|
78
|
+
constructor(network, message) {
|
|
79
|
+
super(
|
|
80
|
+
message ?? `Network configuration error for "${network}". Mainnet requires a pre-funded secretKey (no airdrop available).`
|
|
81
|
+
);
|
|
82
|
+
this.name = "MppNetworkError";
|
|
83
|
+
this.network = network;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
74
86
|
|
|
75
87
|
// src/client.ts
|
|
76
|
-
var
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
var NETWORK_RPC = {
|
|
89
|
+
devnet: "https://api.devnet.solana.com",
|
|
90
|
+
testnet: "https://api.testnet.solana.com",
|
|
91
|
+
mainnet: "https://api.mainnet-beta.solana.com"
|
|
92
|
+
};
|
|
93
|
+
var AIRDROP_NETWORKS = ["devnet", "testnet"];
|
|
94
|
+
async function airdropWithRetry(connection, publicKey, retries = 3) {
|
|
95
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
96
|
+
try {
|
|
97
|
+
const sig = await connection.requestAirdrop(publicKey, 2 * import_web3.LAMPORTS_PER_SOL);
|
|
98
|
+
await connection.confirmTransaction(sig, "confirmed");
|
|
99
|
+
return;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (attempt === retries - 1) {
|
|
102
|
+
throw new MppFaucetError(publicKey.toBase58(), err);
|
|
103
|
+
}
|
|
104
|
+
await new Promise((r) => setTimeout(r, 1e3 * Math.pow(2, attempt)));
|
|
105
|
+
}
|
|
90
106
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
}
|
|
108
|
+
function parseHeaderParams(header) {
|
|
109
|
+
const params = {};
|
|
110
|
+
const parts = header.split(";").map((s) => s.trim());
|
|
111
|
+
for (const part of parts.slice(1)) {
|
|
112
|
+
const eqIdx = part.indexOf("=");
|
|
113
|
+
if (eqIdx > 0) {
|
|
114
|
+
const key = part.slice(0, eqIdx).trim().toLowerCase();
|
|
115
|
+
const val = part.slice(eqIdx + 1).trim().replace(/^"|"$/g, "");
|
|
116
|
+
params[key] = val;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return params;
|
|
120
|
+
}
|
|
121
|
+
function anySignal(signals) {
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
for (const signal of signals) {
|
|
124
|
+
if (signal.aborted) {
|
|
125
|
+
controller.abort(signal.reason);
|
|
126
|
+
return controller.signal;
|
|
127
|
+
}
|
|
128
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
94
129
|
}
|
|
130
|
+
return controller.signal;
|
|
95
131
|
}
|
|
96
132
|
async function createTestClient(config) {
|
|
97
133
|
const emit = config?.onStep ?? (() => {
|
|
98
134
|
});
|
|
99
135
|
const timeout = config?.timeout ?? 3e4;
|
|
100
|
-
const
|
|
101
|
-
const
|
|
136
|
+
const network = config?.network ?? "devnet";
|
|
137
|
+
const rpcUrl = config?.rpcUrl ?? NETWORK_RPC[network];
|
|
138
|
+
if (network === "mainnet" && !config?.secretKey) {
|
|
139
|
+
throw new MppNetworkError(
|
|
140
|
+
"mainnet",
|
|
141
|
+
"createTestClient: mainnet requires a pre-funded secretKey. Airdrop is not available on mainnet. Pass your keypair's secretKey in the config."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const keypair = config?.secretKey ? import_web3.Keypair.fromSecretKey(config.secretKey) : import_web3.Keypair.generate();
|
|
145
|
+
const connection = new import_web3.Connection(rpcUrl, "confirmed");
|
|
146
|
+
const address = keypair.publicKey.toBase58();
|
|
102
147
|
emit({
|
|
103
148
|
type: "wallet-created",
|
|
104
|
-
message: `Wallet ${
|
|
105
|
-
data: { address
|
|
106
|
-
});
|
|
107
|
-
await fundWallet(account.address);
|
|
108
|
-
emit({ type: "funded", message: "Wallet funded on testnet" });
|
|
109
|
-
const mppxClient = import_client.Mppx.create({
|
|
110
|
-
methods: [(0, import_client.tempo)({ account, testnet: true })]
|
|
149
|
+
message: `Wallet ${address}`,
|
|
150
|
+
data: { address, network }
|
|
111
151
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
152
|
+
if (AIRDROP_NETWORKS.includes(network)) {
|
|
153
|
+
await airdropWithRetry(connection, keypair.publicKey);
|
|
154
|
+
emit({
|
|
155
|
+
type: "funded",
|
|
156
|
+
message: `Wallet funded via ${network} airdrop (2 SOL)`,
|
|
157
|
+
data: { network, amount: 2 }
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
emit({
|
|
161
|
+
type: "funded",
|
|
162
|
+
message: "Using pre-funded mainnet wallet",
|
|
163
|
+
data: { network }
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const clientFetch = async (url, init) => {
|
|
167
|
+
emit({ type: "request", message: `\u2192 ${url}`, data: { url } });
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
170
|
+
const signal = init?.signal ? anySignal([init.signal, controller.signal]) : controller.signal;
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(url, { ...init, signal });
|
|
173
|
+
if (res.status !== 402) {
|
|
174
|
+
if (!res.ok) {
|
|
123
175
|
emit({
|
|
124
176
|
type: "error",
|
|
125
|
-
message: `\u2190 ${
|
|
126
|
-
data: { status:
|
|
177
|
+
message: `\u2190 ${res.status} ${res.statusText}`,
|
|
178
|
+
data: { status: res.status }
|
|
127
179
|
});
|
|
128
|
-
throw new MppPaymentError(url,
|
|
180
|
+
throw new MppPaymentError(url, res.status);
|
|
129
181
|
}
|
|
130
182
|
emit({
|
|
131
|
-
type:
|
|
132
|
-
message: `\u2190 ${
|
|
133
|
-
data: { status:
|
|
183
|
+
type: "success",
|
|
184
|
+
message: `\u2190 ${res.status} OK`,
|
|
185
|
+
data: { status: res.status }
|
|
134
186
|
});
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
187
|
+
return res;
|
|
188
|
+
}
|
|
189
|
+
const paymentRequestHeader = res.headers.get("payment-request");
|
|
190
|
+
if (!paymentRequestHeader) {
|
|
191
|
+
throw new MppPaymentError(url, 402, new Error("Server returned 402 without Payment-Request header"));
|
|
192
|
+
}
|
|
193
|
+
const params = parseHeaderParams(paymentRequestHeader);
|
|
194
|
+
if (!params.recipient) {
|
|
195
|
+
throw new MppPaymentError(url, 402, new Error("Payment-Request header missing recipient field"));
|
|
196
|
+
}
|
|
197
|
+
if (!params.amount) {
|
|
198
|
+
throw new MppPaymentError(url, 402, new Error("Payment-Request header missing amount field"));
|
|
199
|
+
}
|
|
200
|
+
const recipient = new import_web3.PublicKey(params.recipient);
|
|
201
|
+
const amountSol = parseFloat(params.amount);
|
|
202
|
+
if (isNaN(amountSol) || amountSol <= 0) {
|
|
203
|
+
throw new MppPaymentError(url, 402, new Error(`Invalid payment amount: ${params.amount}`));
|
|
204
|
+
}
|
|
205
|
+
const lamports = Math.round(amountSol * import_web3.LAMPORTS_PER_SOL);
|
|
206
|
+
emit({
|
|
207
|
+
type: "payment",
|
|
208
|
+
message: `Paying ${amountSol} SOL \u2192 ${params.recipient.slice(0, 8)}...`,
|
|
209
|
+
data: { amount: amountSol, recipient: params.recipient }
|
|
210
|
+
});
|
|
211
|
+
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
212
|
+
const tx = new import_web3.Transaction({
|
|
213
|
+
recentBlockhash: blockhash,
|
|
214
|
+
feePayer: keypair.publicKey
|
|
215
|
+
}).add(
|
|
216
|
+
import_web3.SystemProgram.transfer({
|
|
217
|
+
fromPubkey: keypair.publicKey,
|
|
218
|
+
toPubkey: recipient,
|
|
219
|
+
lamports
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
const signature = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair], {
|
|
223
|
+
commitment: "confirmed"
|
|
224
|
+
});
|
|
225
|
+
emit({
|
|
226
|
+
type: "payment",
|
|
227
|
+
message: `Confirmed: ${signature.slice(0, 16)}...`,
|
|
228
|
+
data: { signature, amount: amountSol }
|
|
229
|
+
});
|
|
230
|
+
emit({
|
|
231
|
+
type: "retry",
|
|
232
|
+
message: `\u2191 Retrying with payment proof`,
|
|
233
|
+
data: { signature }
|
|
234
|
+
});
|
|
235
|
+
const existingHeaders = init?.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init?.headers ?? {};
|
|
236
|
+
const retryRes = await fetch(url, {
|
|
237
|
+
...init,
|
|
238
|
+
signal,
|
|
239
|
+
headers: {
|
|
240
|
+
...existingHeaders,
|
|
241
|
+
"payment-receipt": `solana; signature="${signature}"; network="${network}"; amount="${amountSol}"`
|
|
139
242
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
243
|
+
});
|
|
244
|
+
emit({
|
|
245
|
+
type: retryRes.ok ? "success" : "error",
|
|
246
|
+
message: `\u2190 ${retryRes.status} ${retryRes.ok ? "OK" : retryRes.statusText}`,
|
|
247
|
+
data: { status: retryRes.status, signature }
|
|
248
|
+
});
|
|
249
|
+
return retryRes;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
252
|
+
throw new MppTimeoutError(url, timeout);
|
|
143
253
|
}
|
|
254
|
+
throw err;
|
|
255
|
+
} finally {
|
|
256
|
+
clearTimeout(timer);
|
|
144
257
|
}
|
|
145
258
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return controller.signal;
|
|
153
|
-
}
|
|
154
|
-
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
155
|
-
}
|
|
156
|
-
return controller.signal;
|
|
259
|
+
return {
|
|
260
|
+
address,
|
|
261
|
+
network,
|
|
262
|
+
method: "solana",
|
|
263
|
+
fetch: clientFetch
|
|
264
|
+
};
|
|
157
265
|
}
|
|
158
266
|
var _sharedClient = null;
|
|
159
267
|
async function mppFetch(url, init) {
|
|
@@ -167,32 +275,112 @@ mppFetch.reset = () => {
|
|
|
167
275
|
};
|
|
168
276
|
|
|
169
277
|
// src/server.ts
|
|
170
|
-
var
|
|
171
|
-
var
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
278
|
+
var import_web32 = require("@solana/web3.js");
|
|
279
|
+
var NETWORK_RPC2 = {
|
|
280
|
+
devnet: "https://api.devnet.solana.com",
|
|
281
|
+
testnet: "https://api.testnet.solana.com",
|
|
282
|
+
mainnet: "https://api.mainnet-beta.solana.com"
|
|
283
|
+
};
|
|
284
|
+
function parseHeaderParams2(header) {
|
|
285
|
+
const params = {};
|
|
286
|
+
const parts = header.split(";").map((s) => s.trim());
|
|
287
|
+
for (const part of parts.slice(1)) {
|
|
288
|
+
const eqIdx = part.indexOf("=");
|
|
289
|
+
if (eqIdx > 0) {
|
|
290
|
+
const key = part.slice(0, eqIdx).trim().toLowerCase();
|
|
291
|
+
const val = part.slice(eqIdx + 1).trim().replace(/^"|"$/g, "");
|
|
292
|
+
params[key] = val;
|
|
293
|
+
}
|
|
176
294
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
295
|
+
return params;
|
|
296
|
+
}
|
|
297
|
+
function createTestServer(config = {}) {
|
|
298
|
+
const network = config.network ?? "devnet";
|
|
299
|
+
const rpcUrl = config.rpcUrl ?? NETWORK_RPC2[network];
|
|
300
|
+
const serverKeypair = config.secretKey ? import_web32.Keypair.fromSecretKey(config.secretKey) : import_web32.Keypair.generate();
|
|
301
|
+
const recipientAddress = config.recipientAddress ?? serverKeypair.publicKey.toBase58();
|
|
302
|
+
const connection = new import_web32.Connection(rpcUrl, "confirmed");
|
|
303
|
+
const charge = ({ amount }) => async (req, res, next) => {
|
|
304
|
+
const receiptHeader = req.headers["payment-receipt"] ?? "";
|
|
305
|
+
if (!receiptHeader) {
|
|
306
|
+
res.status(402).set(
|
|
307
|
+
"Payment-Request",
|
|
308
|
+
`solana; amount="${amount}"; recipient="${recipientAddress}"; network="${network}"`
|
|
309
|
+
).json({
|
|
310
|
+
error: "Payment Required",
|
|
311
|
+
payment: {
|
|
312
|
+
amount,
|
|
313
|
+
currency: "SOL",
|
|
314
|
+
recipient: recipientAddress,
|
|
315
|
+
network
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const params = parseHeaderParams2(receiptHeader);
|
|
322
|
+
const { signature } = params;
|
|
323
|
+
if (!signature) {
|
|
324
|
+
res.status(403).json({ error: "Payment-Receipt missing signature field" });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const paidAmount = parseFloat(params.amount ?? "0");
|
|
328
|
+
const requiredAmount = parseFloat(amount);
|
|
329
|
+
if (isNaN(paidAmount) || paidAmount < requiredAmount) {
|
|
330
|
+
res.status(403).json({
|
|
331
|
+
error: `Insufficient payment: claimed ${params.amount ?? "0"} SOL, required ${amount} SOL`
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const tx = await connection.getParsedTransaction(signature, {
|
|
336
|
+
commitment: "confirmed",
|
|
337
|
+
maxSupportedTransactionVersion: 0
|
|
338
|
+
});
|
|
339
|
+
if (!tx) {
|
|
340
|
+
res.status(403).json({ error: "Transaction not found on chain" });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (tx.meta?.err) {
|
|
344
|
+
res.status(403).json({ error: "Transaction failed on chain" });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const accountKeys = tx.transaction.message.accountKeys;
|
|
348
|
+
const preBalances = tx.meta?.preBalances ?? [];
|
|
349
|
+
const postBalances = tx.meta?.postBalances ?? [];
|
|
350
|
+
const recipientPubkey = new import_web32.PublicKey(recipientAddress);
|
|
351
|
+
const recipientIdx = accountKeys.findIndex(
|
|
352
|
+
(k) => k.pubkey.toBase58() === recipientPubkey.toBase58()
|
|
353
|
+
);
|
|
354
|
+
if (recipientIdx < 0) {
|
|
355
|
+
res.status(403).json({
|
|
356
|
+
error: `Recipient ${recipientAddress.slice(0, 8)}... not found in transaction`
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const received = (postBalances[recipientIdx] - preBalances[recipientIdx]) / import_web32.LAMPORTS_PER_SOL;
|
|
361
|
+
if (received < requiredAmount) {
|
|
362
|
+
res.status(403).json({
|
|
363
|
+
error: `Payment too small: received ${received} SOL, required ${requiredAmount} SOL`
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
next();
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
370
|
+
res.status(403).json({ error: `Payment verification failed: ${message}` });
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
return {
|
|
374
|
+
charge,
|
|
375
|
+
recipientAddress,
|
|
376
|
+
network
|
|
377
|
+
};
|
|
191
378
|
}
|
|
192
379
|
// Annotate the CommonJS export names for ESM import in node:
|
|
193
380
|
0 && (module.exports = {
|
|
194
381
|
MppError,
|
|
195
382
|
MppFaucetError,
|
|
383
|
+
MppNetworkError,
|
|
196
384
|
MppPaymentError,
|
|
197
385
|
MppTimeoutError,
|
|
198
386
|
createTestClient,
|