Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/mpp/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"files": {
"ignore": [
"node_modules",
"dist",
".pnpm-store",
"*.lock",
"*.toml",
"**/move/**/build"
]
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "always"
}
}
}
14 changes: 8 additions & 6 deletions packages/mpp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@
"require": "./dist/server.cjs"
}
},
"files": [
"dist",
"README.md"
],
"files": ["dist", "README.md"],
"keywords": [
"mpp",
"sui",
Expand All @@ -49,9 +46,13 @@
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint src/",
"check": "biome check .",
"check:fix": "biome check --write .",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"clean": "rm -rf dist"
},
"dependencies": {
Expand All @@ -60,8 +61,9 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "^1.9.0",
"@types/node": "^20",
"eslint": "^9",
"testcontainers": "^12.0.0",
"tsup": "^8",
"typescript": "^5",
"vitest": "^3"
Expand Down
116 changes: 0 additions & 116 deletions packages/mpp/src/client.test.ts

This file was deleted.

18 changes: 13 additions & 5 deletions packages/mpp/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Method, Credential } from 'mppx';
import type { ClientWithCoreApi } from '@mysten/sui/client';
import type { Signer } from '@mysten/sui/cryptography';
import { coinWithBalance, Transaction } from '@mysten/sui/transactions';
import { Transaction, coinWithBalance } from '@mysten/sui/transactions';
import { Credential, Method } from 'mppx';
import { suiCharge } from './method.js';
import { parseAmountToRaw } from './utils.js';

Expand All @@ -17,6 +17,8 @@ export interface SuiChargeOptions {
execute?: (tx: Transaction) => Promise<{ digest: string }>;
}

type TransactionResult = { digest: string };

export function sui(options: SuiChargeOptions) {
const address = options.signer.toSuiAddress();
const decimals = options.decimals ?? 6;
Expand All @@ -32,7 +34,7 @@ export function sui(options: SuiChargeOptions) {
const payment = coinWithBalance({ balance: amountRaw, type: currency });
tx.transferObjects([payment], recipient);

let result;
let result: TransactionResult;
try {
if (options.execute) {
result = await options.execute(tx);
Expand All @@ -45,9 +47,15 @@ export function sui(options: SuiChargeOptions) {
include: { effects: true },
});
if (execResult.FailedTransaction) {
throw new Error(execResult.FailedTransaction.status.error?.message ?? 'Transaction failed');
throw new Error(
execResult.FailedTransaction.status.error?.message ??
'Transaction failed',
);
}
if (!execResult.Transaction) {
throw new Error('Transaction failed');
}
result = execResult.Transaction!;
result = execResult.Transaction;
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
Expand Down
49 changes: 33 additions & 16 deletions packages/mpp/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Method, Receipt } from 'mppx';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc';
import { normalizeSuiAddress } from '@mysten/sui/utils';
import { Method, Receipt } from 'mppx';
import { InMemoryDigestStore } from './in-memory-digest-store.js';
import { suiCharge } from './method.js';
import { parseAmountToRaw, withRetry } from './utils.js';
import { InMemoryDigestStore } from './in-memory-digest-store.js';

export { suiCharge } from './method.js';
export { SUI_USDC_TYPE } from './constants.js';
Expand All @@ -28,7 +29,7 @@ export interface SuiServerOptions {
/** Number of decimal places for the currency (default: 6, e.g. USDC). */
decimals?: number;
rpcUrl?: string;
network?: 'mainnet' | 'testnet' | 'devnet';
network?: 'mainnet' | 'testnet' | 'devnet' | 'localnet';
/** Digest store for replay protection. Required in production. Falls back to in-memory in dev. */
store?: DigestStore;
/** Called after successful on-chain verification with payment data. */
Expand All @@ -40,19 +41,22 @@ let _defaultStore: DigestStore | undefined;
function resolveStore(options: SuiServerOptions): DigestStore {
if (options.store) return options.store;

if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
if (
typeof process !== 'undefined' &&
process.env?.NODE_ENV === 'production'
) {
throw new Error(
'[suimpp] DigestStore is required in production. ' +
'Provide a Redis or DB-backed store via SuiServerOptions.store. ' +
'The default in-memory store is single-instance only and unsafe for multi-instance deployments.',
'Provide a Redis or DB-backed store via SuiServerOptions.store. ' +
'The default in-memory store is single-instance only and unsafe for multi-instance deployments.',
);
}

if (!_defaultStore) {
_defaultStore = new InMemoryDigestStore();
console.warn(
'[suimpp] No DigestStore provided. Using in-memory store. ' +
'This is NOT safe for production or multi-instance deployments.',
'This is NOT safe for production or multi-instance deployments.',
);
}
return _defaultStore;
Expand All @@ -61,8 +65,13 @@ function resolveStore(options: SuiServerOptions): DigestStore {
export function sui(options: SuiServerOptions) {
const network = options.network ?? 'mainnet';
const decimals = options.decimals ?? 6;
const baseUrl =
options.rpcUrl ??
(network === 'localnet'
? 'http://127.0.0.1:9000'
: getJsonRpcFullnodeUrl(network));
const client = new SuiGrpcClient({
baseUrl: options.rpcUrl ?? `https://fullnode.${network}.sui.io:443`,
baseUrl,
network,
});

Expand All @@ -85,10 +94,15 @@ export function sui(options: SuiServerOptions) {
);
}

const tx = await withRetry(
() => client.core.getTransaction({ digest, include: { balanceChanges: true } }),
const tx = await withRetry(() =>
client.core.getTransaction({
digest,
include: { balanceChanges: true },
}),
).catch(() => {
throw new Error(`Could not find the referenced transaction [${digest}]`);
throw new Error(
`Could not find the referenced transaction [${digest}]`,
);
});

const resolved = tx.Transaction ?? tx.FailedTransaction;
Expand All @@ -104,13 +118,14 @@ export function sui(options: SuiServerOptions) {
);

if (!payment) {
throw new Error(
'Payment not found in transaction balance changes',
);
throw new Error('Payment not found in transaction balance changes');
}

const transferredRaw = BigInt(payment.amount);
const requestedRaw = parseAmountToRaw(credential.challenge.request.amount, decimals);
const requestedRaw = parseAmountToRaw(
credential.challenge.request.amount,
decimals,
);
if (transferredRaw < requestedRaw) {
throw new Error(
`Transferred ${transferredRaw} < requested ${requestedRaw} (raw units)`,
Expand Down Expand Up @@ -138,7 +153,9 @@ export function sui(options: SuiServerOptions) {
};

if (options.onPayment) {
try { options.onPayment(report); } catch {}
try {
options.onPayment(report);
} catch {}
}

return receipt;
Expand Down
5 changes: 4 additions & 1 deletion packages/mpp/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export function parseAmountToRaw(amount: string, decimals: number): bigint {
*/
export async function withRetry<T>(
fn: () => Promise<T>,
{ attempts = 5, baseDelayMs = 1000 }: { attempts?: number; baseDelayMs?: number } = {},
{
attempts = 5,
baseDelayMs = 1000,
}: { attempts?: number; baseDelayMs?: number } = {},
): Promise<T> {
let lastError: unknown;
for (let i = 0; i < attempts; i++) {
Expand Down
Loading