@solana/mpp 0.1.0-beta.0 → 0.1.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/Methods.d.ts +9 -4
- package/dist/Methods.d.ts.map +1 -1
- package/dist/Methods.js +9 -4
- package/dist/Methods.js.map +1 -1
- package/dist/client/Charge.d.ts +6 -4
- package/dist/client/Charge.d.ts.map +1 -1
- package/dist/client/Charge.js +71 -59
- package/dist/client/Charge.js.map +1 -1
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js.map +1 -1
- package/dist/client/Session.d.ts.map +1 -1
- package/dist/client/Session.js.map +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/server/Charge.d.ts +24 -11
- package/dist/server/Charge.d.ts.map +1 -1
- package/dist/server/Charge.js +71 -44
- package/dist/server/Charge.js.map +1 -1
- package/dist/server/Methods.d.ts.map +1 -1
- package/dist/server/Methods.js.map +1 -1
- package/dist/server/Session.d.ts.map +1 -1
- package/dist/server/Session.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/session/ChannelStore.d.ts.map +1 -1
- package/dist/session/ChannelStore.js.map +1 -1
- package/dist/session/Types.d.ts.map +1 -1
- package/dist/session/Types.js.map +1 -1
- package/dist/session/Voucher.d.ts.map +1 -1
- package/dist/session/Voucher.js.map +1 -1
- package/dist/session/authorizers/BudgetAuthorizer.d.ts.map +1 -1
- package/dist/session/authorizers/BudgetAuthorizer.js.map +1 -1
- package/dist/session/authorizers/SwigSessionAuthorizer.d.ts.map +1 -1
- package/dist/session/authorizers/SwigSessionAuthorizer.js.map +1 -1
- package/dist/session/authorizers/UnboundedAuthorizer.d.ts.map +1 -1
- package/dist/session/authorizers/UnboundedAuthorizer.js.map +1 -1
- package/dist/session/authorizers/index.d.ts.map +1 -1
- package/dist/session/authorizers/index.js.map +1 -1
- package/dist/session/authorizers/makeSessionAuthorizer.d.ts.map +1 -1
- package/dist/session/authorizers/makeSessionAuthorizer.js.map +1 -1
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js.map +1 -1
- package/dist/utils/transactions.d.ts +3 -3
- package/dist/utils/transactions.d.ts.map +1 -1
- package/dist/utils/transactions.js +23 -6
- package/dist/utils/transactions.js.map +1 -1
- package/package.json +8 -30
- package/{sdk/src → src}/Methods.ts +13 -4
- package/{sdk/src → src}/client/Charge.ts +93 -76
- package/{sdk/src → src}/index.ts +0 -4
- package/{sdk/src → src}/server/Charge.ts +114 -59
- package/src/utils/transactions.ts +46 -0
- package/LICENSE +0 -21
- package/README.md +0 -227
- package/sdk/src/utils/transactions.ts +0 -26
- /package/{sdk/src → src}/client/Methods.ts +0 -0
- /package/{sdk/src → src}/client/Session.ts +0 -0
- /package/{sdk/src → src}/client/index.ts +0 -0
- /package/{sdk/src → src}/constants.ts +0 -0
- /package/{sdk/src → src}/server/Methods.ts +0 -0
- /package/{sdk/src → src}/server/Session.ts +0 -0
- /package/{sdk/src → src}/server/index.ts +0 -0
- /package/{sdk/src → src}/session/ChannelStore.ts +0 -0
- /package/{sdk/src → src}/session/Types.ts +0 -0
- /package/{sdk/src → src}/session/Voucher.ts +0 -0
- /package/{sdk/src → src}/session/authorizers/BudgetAuthorizer.ts +0 -0
- /package/{sdk/src → src}/session/authorizers/SwigSessionAuthorizer.ts +0 -0
- /package/{sdk/src → src}/session/authorizers/UnboundedAuthorizer.ts +0 -0
- /package/{sdk/src → src}/session/authorizers/index.ts +0 -0
- /package/{sdk/src → src}/session/authorizers/makeSessionAuthorizer.ts +0 -0
- /package/{sdk/src → src}/session/index.ts +0 -0
|
@@ -27,7 +27,7 @@ import { coSignBase64Transaction } from '../utils/transactions.js';
|
|
|
27
27
|
* const mppx = Mppx.create({
|
|
28
28
|
* methods: [solana.charge({
|
|
29
29
|
* recipient: 'RecipientPubkey...',
|
|
30
|
-
*
|
|
30
|
+
* spl: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
31
31
|
* decimals: 6,
|
|
32
32
|
* network: 'devnet',
|
|
33
33
|
* })],
|
|
@@ -43,18 +43,25 @@ import { coSignBase64Transaction } from '../utils/transactions.js';
|
|
|
43
43
|
export function charge(parameters: charge.Parameters) {
|
|
44
44
|
const {
|
|
45
45
|
recipient,
|
|
46
|
-
|
|
46
|
+
currency,
|
|
47
47
|
decimals,
|
|
48
48
|
tokenProgram = TOKEN_PROGRAM,
|
|
49
49
|
network = 'mainnet-beta',
|
|
50
50
|
store = Store.memory(),
|
|
51
|
+
splits,
|
|
51
52
|
signer,
|
|
52
53
|
} = parameters;
|
|
53
54
|
|
|
55
|
+
const isSplToken = currency !== undefined && currency !== 'sol';
|
|
56
|
+
|
|
54
57
|
const rpcUrl = parameters.rpcUrl ?? DEFAULT_RPC_URLS[network] ?? DEFAULT_RPC_URLS['mainnet-beta'];
|
|
55
58
|
|
|
56
|
-
if (
|
|
57
|
-
throw new Error('decimals is required when
|
|
59
|
+
if (isSplToken && decimals === undefined) {
|
|
60
|
+
throw new Error('decimals is required when currency is a token mint address');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (splits && splits.length > 8) {
|
|
64
|
+
throw new Error('splits cannot exceed 8 entries');
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
if (signer && !isTransactionPartialSigner(signer)) {
|
|
@@ -65,10 +72,8 @@ export function charge(parameters: charge.Parameters) {
|
|
|
65
72
|
|
|
66
73
|
return Method.toServer(Methods.charge, {
|
|
67
74
|
defaults: {
|
|
68
|
-
currency:
|
|
69
|
-
methodDetails: {
|
|
70
|
-
reference: '',
|
|
71
|
-
},
|
|
75
|
+
currency: currency ?? 'sol',
|
|
76
|
+
methodDetails: {},
|
|
72
77
|
recipient: '',
|
|
73
78
|
},
|
|
74
79
|
|
|
@@ -77,8 +82,6 @@ export function charge(parameters: charge.Parameters) {
|
|
|
77
82
|
return credential.challenge.request as typeof request;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
const reference = crypto.randomUUID();
|
|
81
|
-
|
|
82
85
|
// Pre-fetch a recent blockhash so the client can skip an RPC call.
|
|
83
86
|
let recentBlockhash: string | undefined;
|
|
84
87
|
try {
|
|
@@ -102,9 +105,9 @@ export function charge(parameters: charge.Parameters) {
|
|
|
102
105
|
...request,
|
|
103
106
|
methodDetails: {
|
|
104
107
|
network,
|
|
105
|
-
|
|
106
|
-
...(splToken ? { decimals, splToken, tokenProgram } : {}),
|
|
108
|
+
...(isSplToken ? { decimals, tokenProgram } : {}),
|
|
107
109
|
...(signer ? { feePayer: true, feePayerKey: signer.address } : {}),
|
|
110
|
+
...(splits?.length ? { splits } : {}),
|
|
108
111
|
...(recentBlockhash ? { recentBlockhash } : {}),
|
|
109
112
|
},
|
|
110
113
|
recipient,
|
|
@@ -243,61 +246,99 @@ async function verifyOnChain(rpcUrl: string, signature: string, challenge: Chall
|
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
async function verifyInstructions(instructions: ParsedInstruction[], challenge: ChallengeRequest, recipient: string) {
|
|
246
|
-
const
|
|
249
|
+
const splits = challenge.methodDetails.splits ?? [];
|
|
250
|
+
const splitsTotal = splits.reduce((sum, s) => sum + BigInt(s.amount), 0n);
|
|
251
|
+
const primaryAmount = BigInt(challenge.amount) - splitsTotal;
|
|
252
|
+
|
|
253
|
+
if (primaryAmount <= 0n) {
|
|
254
|
+
throw new Error('Splits consume the entire amount — primary recipient must receive a positive amount');
|
|
255
|
+
}
|
|
247
256
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
257
|
+
const mint = challenge.currency !== 'sol' ? challenge.currency : undefined;
|
|
258
|
+
|
|
259
|
+
if (mint) {
|
|
260
|
+
// ── SPL token transfers verification ──
|
|
261
|
+
const transfers = instructions.filter(
|
|
251
262
|
ix =>
|
|
252
263
|
ix.parsed?.type === 'transferChecked' &&
|
|
253
264
|
(ix.programId === TOKEN_PROGRAM || ix.programId === TOKEN_2022_PROGRAM),
|
|
254
265
|
);
|
|
255
|
-
if (!transfer) {
|
|
256
|
-
throw new Error('No TransferChecked instruction found in transaction');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const info = transfer.parsed!.info as {
|
|
260
|
-
destination: string;
|
|
261
|
-
mint: string;
|
|
262
|
-
tokenAmount: { amount: string };
|
|
263
|
-
};
|
|
264
|
-
if (info.mint !== challenge.methodDetails.splToken) {
|
|
265
|
-
throw new Error(`Token mint mismatch: expected ${challenge.methodDetails.splToken}, got ${info.mint}`);
|
|
266
|
-
}
|
|
267
|
-
if (info.tokenAmount.amount !== expectedAmount) {
|
|
268
|
-
throw new Error(`Amount mismatch: expected ${expectedAmount}, got ${info.tokenAmount.amount}`);
|
|
269
|
-
}
|
|
270
266
|
|
|
271
|
-
// Verify destination ATA belongs to the expected recipient.
|
|
272
267
|
const expectedTokenProgram = challenge.methodDetails.tokenProgram || TOKEN_PROGRAM;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
268
|
+
|
|
269
|
+
// Verify primary transfer to recipient.
|
|
270
|
+
await verifySplTransfer(transfers, recipient, String(primaryAmount), mint, expectedTokenProgram);
|
|
271
|
+
|
|
272
|
+
// Verify each split transfer.
|
|
273
|
+
for (const split of splits) {
|
|
274
|
+
await verifySplTransfer(transfers, split.recipient, split.amount, mint, expectedTokenProgram);
|
|
280
275
|
}
|
|
281
276
|
} else {
|
|
282
|
-
// ── Native SOL
|
|
283
|
-
const
|
|
284
|
-
if (!transfer) {
|
|
285
|
-
throw new Error('No system transfer instruction found in transaction');
|
|
286
|
-
}
|
|
277
|
+
// ── Native SOL transfers verification ──
|
|
278
|
+
const transfers = instructions.filter(ix => ix.parsed?.type === 'transfer' && ix.program === 'system');
|
|
287
279
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
if (String(info.lamports) !== expectedAmount) {
|
|
296
|
-
throw new Error(`Amount mismatch: expected ${expectedAmount} lamports, got ${info.lamports}`);
|
|
280
|
+
// Verify primary transfer to recipient.
|
|
281
|
+
verifySolTransfer(transfers, recipient, String(primaryAmount));
|
|
282
|
+
|
|
283
|
+
// Verify each split transfer.
|
|
284
|
+
for (const split of splits) {
|
|
285
|
+
verifySolTransfer(transfers, split.recipient, split.amount);
|
|
297
286
|
}
|
|
298
287
|
}
|
|
299
288
|
}
|
|
300
289
|
|
|
290
|
+
async function verifySplTransfer(
|
|
291
|
+
transfers: ParsedInstruction[],
|
|
292
|
+
recipientAddress: string,
|
|
293
|
+
expectedAmount: string,
|
|
294
|
+
spl: string,
|
|
295
|
+
tokenProgram: string,
|
|
296
|
+
) {
|
|
297
|
+
const [expectedAta] = await findAssociatedTokenPda({
|
|
298
|
+
mint: address(spl),
|
|
299
|
+
owner: address(recipientAddress),
|
|
300
|
+
tokenProgram: address(tokenProgram),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const index = transfers.findIndex(ix => {
|
|
304
|
+
const info = ix.parsed!.info as { destination: string; mint: string; tokenAmount: { amount: string } };
|
|
305
|
+
return info.destination === expectedAta && info.mint === spl;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (index === -1) {
|
|
309
|
+
throw new Error(`No TransferChecked instruction found for recipient ${recipientAddress}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const [transfer] = transfers.splice(index, 1);
|
|
313
|
+
|
|
314
|
+
const info = transfer.parsed!.info as { destination: string; mint: string; tokenAmount: { amount: string } };
|
|
315
|
+
if (info.tokenAmount.amount !== expectedAmount) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Amount mismatch for ${recipientAddress}: expected ${expectedAmount}, got ${info.tokenAmount.amount}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function verifySolTransfer(transfers: ParsedInstruction[], recipientAddress: string, expectedAmount: string) {
|
|
323
|
+
const index = transfers.findIndex(ix => {
|
|
324
|
+
const info = ix.parsed!.info as { destination: string };
|
|
325
|
+
return info.destination === recipientAddress;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (index === -1) {
|
|
329
|
+
throw new Error(`No system transfer instruction found for recipient ${recipientAddress}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const [transfer] = transfers.splice(index, 1);
|
|
333
|
+
|
|
334
|
+
const info = transfer.parsed!.info as { destination: string; lamports: number };
|
|
335
|
+
if (String(info.lamports) !== expectedAmount) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Amount mismatch for ${recipientAddress}: expected ${expectedAmount} lamports, got ${info.lamports}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
301
342
|
// ── Types ──
|
|
302
343
|
|
|
303
344
|
/** Credential payload from the mppx framework. */
|
|
@@ -323,8 +364,7 @@ type ChallengeRequest = {
|
|
|
323
364
|
feePayerKey?: string;
|
|
324
365
|
network?: string;
|
|
325
366
|
recentBlockhash?: string;
|
|
326
|
-
|
|
327
|
-
splToken?: string;
|
|
367
|
+
splits?: Array<{ amount: string; memo?: string; recipient: string }>;
|
|
328
368
|
tokenProgram?: string;
|
|
329
369
|
};
|
|
330
370
|
recipient: string;
|
|
@@ -396,6 +436,9 @@ async function simulateTransaction(rpcUrl: string, base64Tx: string): Promise<vo
|
|
|
396
436
|
if (data.error) throw new Error(`RPC error: ${data.error.message}`);
|
|
397
437
|
const simErr = data.result?.value?.err;
|
|
398
438
|
if (simErr) {
|
|
439
|
+
const logs = data.result?.value?.logs ?? [];
|
|
440
|
+
console.error('[solana-mpp] Simulation failed:', JSON.stringify(simErr));
|
|
441
|
+
for (const log of logs) console.error('[solana-mpp]', log);
|
|
399
442
|
throw new Error(`Transaction simulation failed: ${JSON.stringify(simErr)}`);
|
|
400
443
|
}
|
|
401
444
|
}
|
|
@@ -454,7 +497,12 @@ async function waitForConfirmation(rpcUrl: string, signature: string, timeoutMs
|
|
|
454
497
|
|
|
455
498
|
export declare namespace charge {
|
|
456
499
|
type Parameters = {
|
|
457
|
-
/**
|
|
500
|
+
/**
|
|
501
|
+
* Currency identifier. "SOL" for native SOL, or a base58-encoded
|
|
502
|
+
* SPL token mint address (e.g. USDC mint). Defaults to "SOL".
|
|
503
|
+
*/
|
|
504
|
+
currency?: string;
|
|
505
|
+
/** Token decimals (required when currency is a mint address). */
|
|
458
506
|
decimals?: number;
|
|
459
507
|
/** Solana network. Defaults to 'mainnet-beta'. */
|
|
460
508
|
network?: 'devnet' | 'localnet' | 'mainnet-beta' | (string & {});
|
|
@@ -471,14 +519,21 @@ export declare namespace charge {
|
|
|
471
519
|
* Accepts any TransactionPartialSigner — KeyPairSigner, Keychain SolanaSigner, etc.
|
|
472
520
|
*/
|
|
473
521
|
signer?: TransactionPartialSigner;
|
|
474
|
-
/**
|
|
475
|
-
|
|
522
|
+
/** Additional payment splits. Same asset as primary payment. Max 8 entries. */
|
|
523
|
+
splits?: Array<{
|
|
524
|
+
/** Amount in base units (same asset as primary). */
|
|
525
|
+
amount: string;
|
|
526
|
+
/** Optional memo (max 566 bytes). */
|
|
527
|
+
memo?: string;
|
|
528
|
+
/** Base58-encoded recipient of this split. */
|
|
529
|
+
recipient: string;
|
|
530
|
+
}>;
|
|
476
531
|
/**
|
|
477
532
|
* Pluggable key-value store for consumed-signature tracking (replay prevention).
|
|
478
533
|
* Defaults to in-memory. Use a persistent store in production.
|
|
479
534
|
*/
|
|
480
535
|
store?: Store.Store;
|
|
481
|
-
/** Token program
|
|
536
|
+
/** Token program hint. If omitted, clients fetch the mint owner and fail closed on lookup errors. */
|
|
482
537
|
tokenProgram?: string;
|
|
483
538
|
};
|
|
484
539
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Base64EncodedWireTransaction,
|
|
3
|
+
getBase64Codec,
|
|
4
|
+
getBase64EncodedWireTransaction,
|
|
5
|
+
getTransactionDecoder,
|
|
6
|
+
type TransactionPartialSigner,
|
|
7
|
+
} from '@solana/kit';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Decode a base64 wire transaction, co-sign it with a TransactionPartialSigner,
|
|
11
|
+
* and return the co-signed base64 wire transaction.
|
|
12
|
+
*
|
|
13
|
+
* Uses the signer's `signTransactions()` to obtain the signature, then merges
|
|
14
|
+
* it into the decoded transaction. This bridges decoded wire transactions with
|
|
15
|
+
* any signer interface (Keychain, Privy, Turnkey, AWS KMS, etc.).
|
|
16
|
+
*/
|
|
17
|
+
export async function coSignBase64Transaction(
|
|
18
|
+
signer: TransactionPartialSigner,
|
|
19
|
+
clientTxBase64: string,
|
|
20
|
+
): Promise<Base64EncodedWireTransaction> {
|
|
21
|
+
const txBytes = getBase64Codec().encode(clientTxBase64);
|
|
22
|
+
const decoded = getTransactionDecoder().decode(txBytes);
|
|
23
|
+
|
|
24
|
+
// The signer must already be listed in the transaction's signatures map.
|
|
25
|
+
if (decoded.signatures[signer.address] === undefined) {
|
|
26
|
+
throw new Error(`Signer ${signer.address} is not an expected signer for this transaction`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Use the TransactionPartialSigner interface to sign.
|
|
30
|
+
// Cast needed: decoded wire transaction lacks Kit's branded nominal types
|
|
31
|
+
// but is structurally identical (messageBytes + signatures).
|
|
32
|
+
const [signatureMap] = await signer.signTransactions([decoded as Parameters<typeof signer.signTransactions>[0][0]]);
|
|
33
|
+
const signature = signatureMap[signer.address];
|
|
34
|
+
if (!signature) {
|
|
35
|
+
throw new Error(`Signer ${signer.address} did not return a signature`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create a new transaction with the merged signature.
|
|
39
|
+
// Force-cast to preserve Kit's branded nominal types that getBase64EncodedWireTransaction requires.
|
|
40
|
+
const cosigned = {
|
|
41
|
+
...decoded,
|
|
42
|
+
signatures: Object.freeze({ ...decoded.signatures, [signer.address]: signature }),
|
|
43
|
+
} as Parameters<typeof getBase64EncodedWireTransaction>[0];
|
|
44
|
+
|
|
45
|
+
return getBase64EncodedWireTransaction(cosigned);
|
|
46
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 solana-mpp-sdk contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/README.md
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="assets/banner.png" alt="MPP" width="100%" />
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
# @solana/mpp
|
|
6
|
-
|
|
7
|
-
Solana payment method for the [Machine Payments Protocol](https://mpp.dev).
|
|
8
|
-
|
|
9
|
-
**MPP** is [an open protocol proposal](https://paymentauth.org) that lets any HTTP API accept payments using the `402 Payment Required` flow.
|
|
10
|
-
|
|
11
|
-
> [!IMPORTANT]
|
|
12
|
-
> This repository is under active development. The [Solana MPP spec](https://github.com/tempoxyz/mpp-specs/pull/188) is not yet finalized — APIs and wire formats are subject to change.
|
|
13
|
-
|
|
14
|
-
## Install
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
pnpm add @solana/mpp
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Peer dependencies:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
pnpm add @solana/kit mppx
|
|
24
|
-
# Optional — for Swig session authorization:
|
|
25
|
-
pnpm add @swig-wallet/kit
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Features
|
|
29
|
-
|
|
30
|
-
**Charge** (one-time payments)
|
|
31
|
-
- Native SOL and SPL token transfers (USDC, PYUSD, Token-2022, etc.)
|
|
32
|
-
- Two settlement modes: pull (`type="transaction"`, default) and push (`type="signature"`)
|
|
33
|
-
- Fee sponsorship: server pays transaction fees on behalf of clients
|
|
34
|
-
- Replay protection via consumed transaction signatures
|
|
35
|
-
|
|
36
|
-
**Session** (metered / streaming payments)
|
|
37
|
-
- Voucher-based payment channels with monotonic cumulative amounts
|
|
38
|
-
- Multiple authorization modes: `unbounded`, `regular_budget`, `swig_session`
|
|
39
|
-
- Auto-open, auto-topup, and close lifecycle
|
|
40
|
-
- [Swig](https://build.onswig.com) smart wallet integration for on-chain spend limits
|
|
41
|
-
|
|
42
|
-
**General**
|
|
43
|
-
- Works with [ConnectorKit](https://www.connectorkit.dev), `@solana/kit` keypair signers, and [Solana Keychain](https://github.com/solana-foundation/solana-keychain) remote signers
|
|
44
|
-
- Server pre-fetches `recentBlockhash` to save client an RPC round-trip
|
|
45
|
-
- Transaction simulation before broadcast to prevent wasted fees
|
|
46
|
-
|
|
47
|
-
## Architecture
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
sdk/src/
|
|
51
|
-
├── Methods.ts # Shared charge + session schemas
|
|
52
|
-
├── constants.ts # Token programs, USDC mints, RPC URLs
|
|
53
|
-
├── server/
|
|
54
|
-
│ ├── Charge.ts # Server: challenge, verify, broadcast
|
|
55
|
-
│ └── Session.ts # Server: session channel management
|
|
56
|
-
├── client/
|
|
57
|
-
│ ├── Charge.ts # Client: build tx, sign, send
|
|
58
|
-
│ └── Session.ts # Client: session lifecycle
|
|
59
|
-
└── session/
|
|
60
|
-
├── Types.ts # Session types and interfaces
|
|
61
|
-
├── Voucher.ts # Voucher signing and verification
|
|
62
|
-
├── ChannelStore.ts # Persistent channel state
|
|
63
|
-
└── authorizers/ # Pluggable authorization strategies
|
|
64
|
-
├── UnboundedAuthorizer.ts
|
|
65
|
-
├── BudgetAuthorizer.ts
|
|
66
|
-
└── SwigSessionAuthorizer.ts
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**Exports:**
|
|
70
|
-
- `@solana/mpp` — shared schemas, session types, and authorizers
|
|
71
|
-
- `@solana/mpp/server` — server-side charge + session, `Mppx`, `Store`
|
|
72
|
-
- `@solana/mpp/client` — client-side charge + session, `Mppx`
|
|
73
|
-
|
|
74
|
-
## Quick Start
|
|
75
|
-
|
|
76
|
-
### Charge (one-time payment)
|
|
77
|
-
|
|
78
|
-
**Server:**
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
import { Mppx, solana } from '@solana/mpp/server'
|
|
82
|
-
|
|
83
|
-
const mppx = Mppx.create({
|
|
84
|
-
secretKey: process.env.MPP_SECRET_KEY,
|
|
85
|
-
methods: [
|
|
86
|
-
solana.charge({
|
|
87
|
-
recipient: 'RecipientPubkey...',
|
|
88
|
-
splToken: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
89
|
-
decimals: 6,
|
|
90
|
-
}),
|
|
91
|
-
],
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
const result = await mppx.charge({
|
|
95
|
-
amount: '1000000', // 1 USDC
|
|
96
|
-
currency: 'USDC',
|
|
97
|
-
})(request)
|
|
98
|
-
|
|
99
|
-
if (result.status === 402) return result.challenge
|
|
100
|
-
return result.withReceipt(Response.json({ data: '...' }))
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
**Client:**
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
import { Mppx, solana } from '@solana/mpp/client'
|
|
107
|
-
|
|
108
|
-
const mppx = Mppx.create({
|
|
109
|
-
methods: [solana.charge({ signer })], // any TransactionSigner
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const response = await mppx.fetch('https://api.example.com/paid-endpoint')
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Session (metered payments)
|
|
116
|
-
|
|
117
|
-
**Server:**
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
import { Mppx, solana } from '@solana/mpp/server'
|
|
121
|
-
|
|
122
|
-
const mppx = Mppx.create({
|
|
123
|
-
secretKey: process.env.MPP_SECRET_KEY,
|
|
124
|
-
methods: [
|
|
125
|
-
solana.session({
|
|
126
|
-
recipient: 'RecipientPubkey...',
|
|
127
|
-
asset: { kind: 'sol', decimals: 9 },
|
|
128
|
-
channelProgram: 'ChannelProgramId...',
|
|
129
|
-
pricing: { unit: 'request', amountPerUnit: '10', meter: 'api_calls' },
|
|
130
|
-
sessionDefaults: { suggestedDeposit: '1000', ttlSeconds: 60 },
|
|
131
|
-
}),
|
|
132
|
-
],
|
|
133
|
-
})
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Client:**
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
import { Mppx, solana } from '@solana/mpp/client'
|
|
140
|
-
import { UnboundedAuthorizer } from '@solana/mpp'
|
|
141
|
-
|
|
142
|
-
const mppx = Mppx.create({
|
|
143
|
-
methods: [
|
|
144
|
-
solana.session({
|
|
145
|
-
signer,
|
|
146
|
-
authorizer: new UnboundedAuthorizer({ signer, buildOpenTx, buildTopupTx }),
|
|
147
|
-
}),
|
|
148
|
-
],
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
const response = await mppx.fetch('https://api.example.com/metered-endpoint')
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Fee Sponsorship (charge)
|
|
155
|
-
|
|
156
|
-
The server can pay transaction fees on behalf of clients:
|
|
157
|
-
|
|
158
|
-
```ts
|
|
159
|
-
// Server — pass a TransactionPartialSigner to cover fees
|
|
160
|
-
solana.charge({
|
|
161
|
-
recipient: '...',
|
|
162
|
-
signer: feePayerSigner, // KeyPairSigner, Keychain SolanaSigner, etc.
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// Client — no changes needed, fee payer is handled automatically
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
## How It Works
|
|
169
|
-
|
|
170
|
-
### Charge Flow
|
|
171
|
-
|
|
172
|
-
1. Client requests a resource
|
|
173
|
-
2. Server returns **402 Payment Required** with a challenge (`recipient`, `amount`, `currency`, `recentBlockhash`)
|
|
174
|
-
3. Client builds and signs a Solana transfer transaction
|
|
175
|
-
4. Server simulates, broadcasts, confirms on-chain, and verifies the transfer
|
|
176
|
-
5. Server returns the resource with a `Payment-Receipt` header
|
|
177
|
-
|
|
178
|
-
With fee sponsorship, the client partially signs (transfer authority only) and the server co-signs as fee payer before broadcasting.
|
|
179
|
-
|
|
180
|
-
### Session Flow
|
|
181
|
-
|
|
182
|
-
1. First request: server returns 402, client opens a channel (deposit + voucher)
|
|
183
|
-
2. Subsequent requests: client sends updated vouchers with monotonic cumulative amounts
|
|
184
|
-
3. Server deducts from the channel balance per its pricing config
|
|
185
|
-
4. When balance runs low: client tops up the channel
|
|
186
|
-
5. On close: final voucher settles the channel
|
|
187
|
-
|
|
188
|
-
## Demo
|
|
189
|
-
|
|
190
|
-
An interactive playground with a React frontend and Express backend, running against [Surfpool](https://surfpool.run).
|
|
191
|
-
|
|
192
|
-
- Charge flow demo: `http://localhost:5173/charges`
|
|
193
|
-
- Session flow demo: `http://localhost:5173/sessions`
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
surfpool start
|
|
197
|
-
pnpm demo:install
|
|
198
|
-
pnpm demo:server
|
|
199
|
-
pnpm demo:app
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
See [demo/README.md](demo/README.md) for full details.
|
|
203
|
-
|
|
204
|
-
## Development
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
pnpm install
|
|
208
|
-
|
|
209
|
-
just fmt # Format and lint
|
|
210
|
-
just build # Typecheck
|
|
211
|
-
just test # Unit tests (charge + session, no network)
|
|
212
|
-
just test-integration # Integration tests (requires Surfpool)
|
|
213
|
-
just test-all # All tests
|
|
214
|
-
just pre-commit # fmt + typecheck + unit tests
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
## Spec
|
|
218
|
-
|
|
219
|
-
This SDK implements the [Solana Charge Intent](https://github.com/tempoxyz/mpp-specs/pull/188) for the [HTTP Payment Authentication Scheme](https://paymentauth.org).
|
|
220
|
-
|
|
221
|
-
Session method docs and implementation notes:
|
|
222
|
-
|
|
223
|
-
- [docs/methods/sessions.md](docs/methods/sessions.md)
|
|
224
|
-
|
|
225
|
-
## License
|
|
226
|
-
|
|
227
|
-
MIT
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Base64EncodedWireTransaction,
|
|
3
|
-
getBase64Codec,
|
|
4
|
-
getBase64EncodedWireTransaction,
|
|
5
|
-
getTransactionDecoder,
|
|
6
|
-
partiallySignTransactionWithSigners,
|
|
7
|
-
type TransactionPartialSigner,
|
|
8
|
-
} from '@solana/kit';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Decode a base64 wire transaction, co-sign it with a TransactionPartialSigner,
|
|
12
|
-
* and return the co-signed base64 wire transaction.
|
|
13
|
-
*
|
|
14
|
-
* Uses Kit's `partiallySignTransactionWithSigners` to handle signature merging
|
|
15
|
-
* and validation. This bridges decoded wire transactions with the signer
|
|
16
|
-
* interface (Keychain, Privy, Turnkey, AWS KMS, etc.).
|
|
17
|
-
*/
|
|
18
|
-
export async function coSignBase64Transaction(
|
|
19
|
-
signer: TransactionPartialSigner,
|
|
20
|
-
clientTxBase64: string,
|
|
21
|
-
): Promise<Base64EncodedWireTransaction> {
|
|
22
|
-
const txBytes = getBase64Codec().encode(clientTxBase64);
|
|
23
|
-
const tx = getTransactionDecoder().decode(txBytes);
|
|
24
|
-
const cosigned = await partiallySignTransactionWithSigners([signer], tx);
|
|
25
|
-
return getBase64EncodedWireTransaction(cosigned);
|
|
26
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|