Initial commit

This commit is contained in:
defiQUG
2026-01-01 08:04:06 -08:00
commit d0bc005be1
75 changed files with 15082 additions and 0 deletions

43
packages/x402/README.md Normal file
View File

@@ -0,0 +1,43 @@
# @dbis-thirdweb/x402
x402 payment primitives and pay-to-access flows for Chain 138.
## Usage
### Pay-to-Access Flow
```typescript
import { PayToAccessFlow, InMemoryReplayProtectionStore } from '@dbis-thirdweb/x402';
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
import { chain138 } from '@dbis-thirdweb/chain';
// Server-side: Create request
const sdk = new ThirdwebSDK(chain138);
const provider = sdk.getProvider();
const replayStore = new InMemoryReplayProtectionStore();
const flow = new PayToAccessFlow(provider, replayStore);
const request = await flow.createRequest({
amount: ethers.utils.parseEther('0.01'),
recipient: '0x...',
expiresInSeconds: 3600,
});
const challenge = flow.generateChallenge(request);
// Client-side: Fulfill payment
const signer = await wallet.getSigner();
const receipt = await flow.fulfillPayment(challenge, signer);
// Server-side: Verify payment
const isValid = await flow.verifyPayment(receipt, request);
```
## Features
- Payment request creation and validation
- Replay protection via request ID tracking
- Receipt verification on-chain
- Pay-to-access flow implementation
- Expiration handling

View File

@@ -0,0 +1,38 @@
{
"name": "@dbis-thirdweb/x402",
"version": "0.1.0",
"description": "x402 payment primitives and pay-to-access flows for Chain 138",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"lint": "eslint src",
"test": "echo \"No tests yet\""
},
"keywords": [
"thirdweb",
"x402",
"payment",
"chain-138"
],
"author": "",
"license": "MIT",
"dependencies": {
"@dbis-thirdweb/chain": "workspace:*",
"@thirdweb-dev/sdk": "^4.0.0",
"ethers": "^5.7.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}

View File

@@ -0,0 +1,118 @@
import type { Signer, providers } from 'ethers';
import { ethers } from 'ethers';
import type { PaymentRequest, PaymentChallenge, PaymentReceipt } from '../types';
import { validateRequest, type ReplayProtectionStore } from '../replayProtection';
import { verifyReceipt, waitForReceipt } from '../receiptVerification';
import { chain138 } from '@dbis-thirdweb/chain';
/**
* Generate payment challenge for x402 pay-to-access flow
*/
export function generateChallenge(request: PaymentRequest): PaymentChallenge {
const nonce = ethers.utils.hexlify(ethers.utils.randomBytes(32));
// Create message to sign (includes request details + nonce for replay protection)
const message = JSON.stringify({
requestId: request.requestId,
amount: request.amount.toString(),
recipient: request.recipient,
expiresAt: request.expiresAt,
nonce,
chainId: chain138.chainId,
});
return {
request,
nonce,
message,
};
}
/**
* x402 Pay-to-Access Flow
* HTTP request → x402 challenge → settlement on-chain
*/
export class PayToAccessFlow {
constructor(
private provider: providers.Provider,
private replayStore: ReplayProtectionStore
) {}
/**
* Create a payment request (server-side)
*/
async createRequest(params: {
amount: bigint;
recipient: string;
expiresInSeconds?: number;
metadata?: string;
}): Promise<PaymentRequest> {
const requestId = ethers.utils.hexlify(ethers.utils.randomBytes(32));
const expiresAt = Math.floor(Date.now() / 1000) + (params.expiresInSeconds || 3600);
const request: PaymentRequest = {
requestId,
amount: params.amount,
recipient: params.recipient,
expiresAt,
metadata: params.metadata,
};
// Validate request (check for expiration and replay)
await validateRequest(request, this.replayStore);
return request;
}
/**
* Generate challenge from request (server-side)
*/
generateChallenge(request: PaymentRequest): PaymentChallenge {
return generateChallenge(request);
}
/**
* Fulfill payment (client-side with signer)
*/
async fulfillPayment(
challenge: PaymentChallenge,
signer: Signer
): Promise<PaymentReceipt> {
// Validate request before processing
await validateRequest(challenge.request, this.replayStore);
// Create and send transaction
const tx = await signer.sendTransaction({
to: challenge.request.recipient,
value: challenge.request.amount,
});
// Wait for confirmation
const receipt = await waitForReceipt(tx.hash, this.provider);
// Mark request as used to prevent replay
await this.replayStore.markAsUsed(challenge.request.requestId);
return {
...receipt,
requestId: challenge.request.requestId,
};
}
/**
* Verify payment receipt (server-side)
*/
async verifyPayment(
receipt: PaymentReceipt,
originalRequest: PaymentRequest
): Promise<boolean> {
const isValid = await verifyReceipt(receipt, originalRequest, this.provider);
if (isValid) {
// Mark as used if verification succeeds
await this.replayStore.markAsUsed(originalRequest.requestId);
}
return isValid;
}
}

View File

@@ -0,0 +1,4 @@
export * from './types';
export * from './replayProtection';
export * from './receiptVerification';
export * from './flows/payToAccess';

View File

@@ -0,0 +1,72 @@
import type { providers } from 'ethers';
import type { PaymentReceipt, PaymentRequest } from './types';
import { chain138 } from '@dbis-thirdweb/chain';
/**
* Verify payment receipt on-chain
*/
export async function verifyReceipt(
receipt: PaymentReceipt,
request: PaymentRequest,
provider: providers.Provider
): Promise<boolean> {
try {
// Get transaction receipt
const txReceipt = await provider.getTransactionReceipt(receipt.txHash);
if (!txReceipt) {
return false;
}
// Verify transaction is on correct chain
// Note: Provider should already be configured for Chain 138
// Verify transaction succeeded
if (txReceipt.status !== 1) {
return false;
}
// Verify block number matches
if (txReceipt.blockNumber !== receipt.blockNumber) {
return false;
}
// Verify amount was transferred (check logs or transaction value)
// This is a simplified check - in production, you'd verify specific logs/events
const tx = await provider.getTransaction(receipt.txHash);
if (tx && tx.value.toString() !== request.amount.toString()) {
return false;
}
return true;
} catch (error) {
console.error('Error verifying receipt:', error);
return false;
}
}
/**
* Wait for payment receipt confirmation
*/
export async function waitForReceipt(
txHash: string,
provider: providers.Provider,
confirmations: number = 1
): Promise<PaymentReceipt> {
const receipt = await provider.waitForTransaction(txHash, confirmations);
if (!receipt) {
throw new Error(`Transaction ${txHash} not found`);
}
if (receipt.status !== 1) {
throw new Error(`Transaction ${txHash} failed`);
}
return {
txHash: receipt.transactionHash,
blockNumber: receipt.blockNumber,
requestId: '', // Should be extracted from transaction data/logs
confirmedAt: Math.floor(Date.now() / 1000),
};
}

View File

@@ -0,0 +1,50 @@
import type { PaymentRequest } from './types';
/**
* Storage interface for replay protection
*/
export interface ReplayProtectionStore {
hasBeenUsed(requestId: string): Promise<boolean>;
markAsUsed(requestId: string): Promise<void>;
}
/**
* In-memory replay protection store (for testing/single-instance use)
*/
export class InMemoryReplayProtectionStore implements ReplayProtectionStore {
private usedRequests = new Set<string>();
async hasBeenUsed(requestId: string): Promise<boolean> {
return this.usedRequests.has(requestId);
}
async markAsUsed(requestId: string): Promise<void> {
this.usedRequests.add(requestId);
}
}
/**
* Check if payment request has expired
*/
export function isRequestExpired(request: PaymentRequest): boolean {
const now = Math.floor(Date.now() / 1000);
return request.expiresAt < now;
}
/**
* Validate payment request for replay protection
*/
export async function validateRequest(
request: PaymentRequest,
store: ReplayProtectionStore
): Promise<void> {
// Check expiration
if (isRequestExpired(request)) {
throw new Error(`Payment request ${request.requestId} has expired`);
}
// Check if already used
if (await store.hasBeenUsed(request.requestId)) {
throw new Error(`Payment request ${request.requestId} has already been used`);
}
}

View File

@@ -0,0 +1,76 @@
import type { BigNumberish } from 'ethers';
/**
* x402 Payment Request
*/
export interface PaymentRequest {
/**
* Unique request ID (prevents replay attacks)
*/
requestId: string;
/**
* Payment amount in native currency (wei)
*/
amount: BigNumberish;
/**
* Recipient address
*/
recipient: string;
/**
* Timestamp when request expires (Unix timestamp in seconds)
*/
expiresAt: number;
/**
* Optional metadata or description
*/
metadata?: string;
}
/**
* x402 Payment Challenge
*/
export interface PaymentChallenge {
/**
* Original request
*/
request: PaymentRequest;
/**
* Nonce for replay protection
*/
nonce: string;
/**
* Challenge message to sign
*/
message: string;
}
/**
* x402 Payment Receipt
*/
export interface PaymentReceipt {
/**
* Transaction hash
*/
txHash: string;
/**
* Block number where transaction was confirmed
*/
blockNumber: number;
/**
* Request ID that was fulfilled
*/
requestId: string;
/**
* Timestamp of confirmation
*/
confirmedAt: number;
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": false
},
"include": ["src/**/*"],
"references": [
{ "path": "../chain" }
]
}