Skip to content
Open
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,52 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

## Private USDC CLI

This repo now includes a CLI for repeating the same private USDC transfer without going through the UI flow.

```bash
./ppay <amount> <from-keypair.json> <to-wallet> <repeat>
```

Example:

```bash
./ppay 1 Ae4mRHxCtxSbxvxSontR3DTQSkwP49e3sGxrK6BSXkam.json Me4mRHxCtxSbxvxSontR3DTQSkwP49e3sGxrK6BSXkam 20
```

If you want `ppay` available directly on your shell path, run:

```bash
npm link
```

Then you can use:

```bash
ppay 1 <from-keypair.json> <to-wallet> 20
```

The CLI always sends USDC and always builds a private transfer. The `from` argument is the path to the sender secret-key file, and the script derives the sender pubkey from that file.

Accepted `from` file formats:

- JSON array of secret-key bytes
- JSON string containing a base58-encoded secret key
- Raw base58-encoded secret key text

Useful env vars:

- `PAYMENTS_API_BASE_URL`
- `PAYMENTS_CLUSTER`
- `PAYMENTS_USDC_MINT`
- `SOLANA_RPC_URL`
- `PPAY_MIN_DELAY_MS`
- `PPAY_MAX_DELAY_MS`
- `PPAY_SPLIT`
- `PPAY_MEMO`


## Learn More

To learn more, take a look at the following resources:
Expand Down
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
"name": "magicblock-pay",
"version": "0.1.0",
"private": true,
"type": "module",
"bin": {
"ppay": "./ppay",
"ppaymulti": "./ppaymulti",
"txanalyzer": "./txanalyzer"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint ."
"lint": "eslint .",
"ppay": "node --experimental-strip-types ./scripts/ppay.ts",
"ppaymulti": "node --experimental-strip-types ./scripts/ppaymulti.ts",
"txanalyzer": "node --experimental-strip-types ./scripts/txanalyzer.ts"
},
"dependencies": {
"@bonfida/spl-name-service": "^3.0.20",
Expand Down Expand Up @@ -45,6 +54,7 @@
"@solana/web3.js": "^1.98.0",
"@vercel/analytics": "1.6.1",
"autoprefixer": "^10.4.20",
"bs58": "^6.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.1.1",
Expand Down
5 changes: 5 additions & 0 deletions ppay
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
exec node --experimental-strip-types "$SCRIPT_DIR/scripts/ppay.ts" "$@"
5 changes: 5 additions & 0 deletions ppaymulti
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
exec node --experimental-strip-types "$SCRIPT_DIR/scripts/ppaymulti.ts" "$@"
61 changes: 61 additions & 0 deletions scripts/network-retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const MAX_BACKOFF_DELAY_MS = 60_000;
const BACKOFF_STEP_THRESHOLD_MS = 16_000;
const INITIAL_BACKOFF_MIN_MS = 500;
const INITIAL_BACKOFF_MAX_MS = 900;

export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

function getRandomInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

export function getBackoffDelayMs(attempt: number) {
let minDelayMs = INITIAL_BACKOFF_MIN_MS;
let maxDelayMs = INITIAL_BACKOFF_MAX_MS;

for (let currentAttempt = 1; currentAttempt < attempt; currentAttempt += 1) {
if (maxDelayMs < BACKOFF_STEP_THRESHOLD_MS) {
minDelayMs = Math.min(BACKOFF_STEP_THRESHOLD_MS, minDelayMs * 2);
maxDelayMs = Math.min(BACKOFF_STEP_THRESHOLD_MS, maxDelayMs * 2);
} else {
minDelayMs = Math.min(MAX_BACKOFF_DELAY_MS, minDelayMs + 4_000);
maxDelayMs = Math.min(MAX_BACKOFF_DELAY_MS, maxDelayMs + 4_000);
}
}

return getRandomInteger(
Math.min(MAX_BACKOFF_DELAY_MS, minDelayMs),
Math.min(MAX_BACKOFF_DELAY_MS, maxDelayMs)
);
}

export function isRetriableNetworkError(message: string) {
return /too many requests|429|fetch failed|timed out|timeout|network/i.test(message);
}

export async function withNetworkRetry<T>(
fn: () => Promise<T>,
onRetry: (info: { attempt: number; delayMs: number; message: string }) => void,
shouldRetry?: (message: string) => boolean
): Promise<T> {
let attempt = 1;

while (true) {
try {
return await fn();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const retryable = shouldRetry ? shouldRetry(message) : isRetriableNetworkError(message);
if (!retryable) {
throw error;
}

const delayMs = getBackoffDelayMs(attempt);
onRetry({ attempt, delayMs, message });
await sleep(delayMs);
attempt += 1;
}
}
}
Loading
Loading