Skip to content
Merged
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ This is not a hosted dashboard or production queue yet. It is a developer-facing
| CLI scaffolder | Ready in repo | `packages/create-arc-nano-kit` generates Express or Next.js paid API starters. |
| Arc Receipts | Ready | Invoices, memos, receipts, signed webhook events, and in-memory ledger. |
| Arc Testnet watcher | Ready | Watches memo-wrapped USDC payments and records matching receipts locally. |
| Arc Testnet proof mode | Ready | Verifies a pasted tx hash against a memo payment request and returns block/log proof. |
| Webhook Inbox + Replay | Ready | Verifies signed webhook delivery attempts and replays events locally. |
| Demo app | Ready locally | Next.js demo with paid endpoints, watcher flow, inbox verification, and replay. |
| Demo app | Ready locally | Next.js demo with paid endpoints, watcher flow, onchain proof, inbox verification, and replay. |
| Persistent receipt store | Planned | SQLite/Postgres adapter is a next-step production feature. |
| Hosted dashboard | Planned | Analytics UI and managed ops surface are not part of the current MVP. |
| Fastify/Hono/Python/Go adapters | Planned | Current framework adapters are Express and Next.js. |
Expand Down Expand Up @@ -116,6 +117,7 @@ sequenceDiagram
App->>Arc: send USDC with memo
SDK->>Arc: watcher polls Memo events
Arc-->>SDK: matching memo + transfer
SDK->>Arc: verify tx receipt proof
SDK->>SDK: generate receipt
SDK->>SDK: sign invoice.paid webhook
SDK->>Inbox: deliver signed payload
Expand All @@ -129,6 +131,7 @@ sequenceDiagram
The demo does not stop at "webhook ready". It shows verified delivery:

- `receipt.generated`
- optional `onchainProof` with tx hash, block, log index, and Arcscan link
- raw webhook payload
- `x-arc-signature`
- SDK verification
Expand Down Expand Up @@ -349,6 +352,7 @@ import {
createInvoiceMemo,
createMemoPaymentRequest,
signWebhookEvent,
verifyMemoPaymentProof,
verifyWebhookSignature,
} from '@arc-nano-kit/sdk/receipts';
```
Expand Down
2 changes: 2 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [x] In-memory receipt ledger
- [x] HMAC-signed webhook events
- [x] Arc Testnet watcher for memo-wrapped USDC payments
- [x] Read-only Arc Testnet tx proof for memo payment requests
- [x] Local webhook inbox
- [x] Webhook replay attempts with fresh signature timestamps

Expand All @@ -39,6 +40,7 @@
- [x] Local Next.js demo app
- [x] Interactive watcher flow in the demo
- [x] Webhook Inbox + Replay demo flow
- [x] Optional Onchain Proof demo panel
- [x] Repo-local `create-arc-nano-kit` scaffolder
- [x] Grant snapshot and demo script docs

Expand Down
13 changes: 9 additions & 4 deletions apps/demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ invoice
-> transaction memo payment request
-> watcher flow
-> generated receipt
-> optional Arc Testnet tx proof
-> signed webhook
-> local inbox verification
-> replayed delivery attempt
Expand All @@ -29,10 +30,12 @@ Open [http://localhost:3000](http://localhost:3000).
1. Click `Run Watcher Flow`.
2. Inspect `Memo Payment Data`.
3. Inspect `Generated Receipt`.
4. Inspect `Webhook Inbox`.
5. Confirm `Received`, `Verified`, and `Signature OK`.
6. Click `Replay Webhook`.
7. Confirm `Delivery attempt #2` appears with a fresh signature timestamp.
4. Inspect `Onchain Proof`.
5. Optionally paste a real Arc Testnet Memo transaction hash and verify it.
6. Inspect `Webhook Inbox`.
7. Confirm `Received`, `Verified`, and `Signature OK`.
8. Click `Replay Webhook`.
9. Confirm `Delivery attempt #2` appears with a fresh signature timestamp.

## Demo Routes

Expand All @@ -41,6 +44,7 @@ Open [http://localhost:3000](http://localhost:3000).
| `GET /api/joke` | Paywalled joke endpoint probe |
| `GET /api/weather?city=NYC` | Paywalled weather endpoint probe |
| `GET /api/receipts` | Generates the local receipt/watcher demo payload |
| `POST /api/receipts/proof` | Verifies a pasted Arc Testnet tx hash against the memo payment request |
| `POST /api/webhook-inbox` | Receives raw signed webhook payload and verifies it |
| `POST /api/webhook-inbox/replay` | Replays a webhook event with a fresh signature timestamp |

Expand All @@ -52,6 +56,7 @@ See [../../docs/demo-script.md](../../docs/demo-script.md) for the full grant-re

- The inbox is in-memory.
- The receipt ledger is in-memory.
- Onchain proof mode is read-only and never asks for a private key.
- The watcher flow is a local developer demo.
- Hosted dashboard and persistent storage are planned, not shipped.

Expand Down
97 changes: 97 additions & 0 deletions apps/demo/src/app/api/receipts/proof/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
MemoPaymentProofError,
createReceipt,
stablecoinUnitsToString,
verifyMemoPaymentProof,
type ArcInvoice,
type MemoPaymentRequest,
} from '@arc-nano-kit/sdk/receipts';

export const dynamic = 'force-dynamic';

interface ProofRequest {
txHash?: string;
invoice?: ArcInvoice;
paymentRequest?: MemoPaymentRequest;
}

export async function POST(request: Request) {
const body = (await request.json()) as ProofRequest;

if (!body.txHash || !isTxHash(body.txHash)) {
return Response.json(
{ error: 'Missing or invalid Arc Testnet tx hash', reason: 'invalid_tx_hash' },
{ status: 400 },
);
}

if (!body.invoice || !body.paymentRequest) {
return Response.json(
{ error: 'Missing invoice or payment request', reason: 'missing_payment_request' },
{ status: 400 },
);
}

try {
const proof = await verifyMemoPaymentProof({
txHash: body.txHash,
paymentRequest: body.paymentRequest,
});
const receipt = createReceipt(body.invoice, {
txHash: proof.txHash,
from: proof.payer,
to: proof.payTo,
amount: stablecoinUnitsToString(BigInt(proof.amountUnits)),
currency: body.invoice.currency,
network: proof.network,
memo: body.invoice.memo,
memoId: proof.memoId,
callDataHash: proof.callDataHash,
blockNumber: proof.blockNumber,
onchainProof: proof,
metadata: {
source: 'arc-testnet-proof',
memoContract: proof.memoContract,
memoIndex: proof.memoIndex,
explorerUrl: proof.explorerUrl,
},
});

return Response.json(toJsonSafe({ proof, receipt }));
} catch (error) {
if (error instanceof MemoPaymentProofError) {
return Response.json(
{ error: error.message, reason: error.reason },
{ status: 422 },
);
}

const message = error instanceof Error ? error.message : 'Unknown proof verification error';
return Response.json(
{ error: message, reason: 'proof_verification_failed' },
{ status: 500 },
);
}
}

function isTxHash(value: string): value is `0x${string}` {
return /^0x[0-9a-fA-F]{64}$/.test(value);
}

function toJsonSafe(value: unknown): unknown {
if (typeof value === 'bigint') {
return value.toString();
}

if (Array.isArray(value)) {
return value.map((item) => toJsonSafe(item));
}

if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, nested]) => [key, toJsonSafe(nested)]),
);
}

return value;
}
Loading
Loading