-
Notifications
You must be signed in to change notification settings - Fork 1k
[Feat] Java implementation #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
e7daa42
chore(java): .gitignore
stefanoamorelli 0c41f52
chore(java): project object model
stefanoamorelli dd19ecf
chore(java): check style rules
stefanoamorelli 8d4bf63
chore(java): check style rule suppressions
stefanoamorelli 39196e9
chore(java): spotbugs exclude rules
stefanoamorelli 580f240
feat(java): json util function
stefanoamorelli c176593
feat(java): implement payment payload model
stefanoamorelli 77eeb79
feat(java): implement payment required response
stefanoamorelli ff3ef4e
feat(java): implement payment requirements model
stefanoamorelli 406865a
feat(java): implement draft crypto signer interface
stefanoamorelli 49ed01b
feat(java): implement payment kind class
stefanoamorelli 81f6f99
feat(java): implement SettlementResponse class
stefanoamorelli 4fcabe1
feat(java): implement VerificationResponse class
stefanoamorelli 98233ab
feat(java): implement facilitator client
stefanoamorelli 4fd6ac0
feat(java): implement X402HttpClient
stefanoamorelli 2f82f19
feat(java): implement PaymentFilter
stefanoamorelli 6415990
test(java): add integration test
stefanoamorelli 7a1cfd2
feat(java): expand exception handling in paymentfilter
stefanoamorelli 29399ca
chore(java): improve javadocs
stefanoamorelli b59dc91
doc(java): add README.md
stefanoamorelli 00f7962
refactor(crypto): introduce protocol-specific signing rules for Crypt…
stefanoamorelli 80b31aa
ci(java): setup java ci with maven and tests
stefanoamorelli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| name: Java CI | ||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'java/**' | ||
| push: | ||
| branches: [ main ] | ||
| paths: | ||
| - 'java/**' | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up JDK 17 | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
|
|
||
| - name: Cache Maven dependencies | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: ~/.m2 | ||
| key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} | ||
| restore-keys: ${{ runner.os }}-m2 | ||
|
|
||
| - name: Run tests with coverage | ||
| working-directory: ./java | ||
| run: mvn clean test -Pcoverage | ||
|
|
||
| - name: Run checkstyle | ||
| working-directory: ./java | ||
| run: mvn checkstyle:check | ||
|
|
||
| - name: Run SpotBugs | ||
| working-directory: ./java | ||
| run: mvn spotbugs:check |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # Maven | ||
| target/ | ||
| pom.xml.tag | ||
| pom.xml.releaseBackup | ||
| pom.xml.versionsBackup | ||
| pom.xml.next | ||
| release.properties | ||
| dependency-reduced-pom.xml | ||
| buildNumber.properties | ||
| .mvn/timing.properties | ||
| .mvn/wrapper/maven-wrapper.jar | ||
|
|
||
| # Eclipse | ||
| .classpath | ||
| .project | ||
| .settings/ | ||
|
|
||
| # IntelliJ IDEA | ||
| .idea/ | ||
| *.iws | ||
| *.iml | ||
| *.ipr | ||
|
|
||
| # NetBeans | ||
| /nbproject/private/ | ||
| /nbbuild/ | ||
| /dist/ | ||
| /nbdist/ | ||
| /.nb-gradle/ | ||
| /build/ | ||
|
|
||
| # VS Code | ||
| .vscode/ | ||
|
|
||
| # Mac | ||
| .DS_Store | ||
|
|
||
| # Compile files | ||
| *.class | ||
|
|
||
| # Log files | ||
| *.log | ||
| logs/ | ||
|
|
||
| # BlueJ files | ||
| *.ctxt | ||
|
|
||
| # Package Files | ||
| *.jar | ||
| *.war | ||
| *.nar | ||
| *.ear | ||
| *.zip | ||
| *.tar.gz | ||
| *.rar | ||
|
|
||
| # Virtual Machine crash logs | ||
| hs_err_pid* | ||
|
|
||
| # Local config overrides | ||
| application-local.properties | ||
| application-local.yml |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,286 @@ | ||
| # x402 Java | ||
|
|
||
| [](https://github.com/coinbase/x402/java) | ||
| [](https://github.com/coinbase/x402/blob/main/LICENSE) | ||
| [](https://github.com/coinbase/x402/java) | ||
|
|
||
| Java implementation of [x402](https://github.com/coinbase/x402) | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ```bash | ||
| # Build and test | ||
| mvn clean install | ||
|
|
||
| # Run tests | ||
| mvn test | ||
|
|
||
| # Check code coverage | ||
| mvn jacoco:report | ||
| mvn -P coverage verify # Enforces 90% coverage | ||
|
|
||
| # Check code quality | ||
| mvn checkstyle:check | ||
| mvn spotbugs:check | ||
| ``` | ||
|
|
||
| ## Overview | ||
|
|
||
| x402 is a system for decentralized payments for API calls, web content, and other HTTP resources. The `402` stands for the HTTP status code `Payment Required`. | ||
|
|
||
| This library provides a Java implementation of the `x402` protocol, with the following core components: | ||
|
|
||
| - `**PaymentFilter**`: A servlet filter that authenticates payments and rejects unauthorized requests | ||
| - `**FacilitatorClient**`: A client for verifying and settling payments with a facilitator service | ||
| - `**X402HttpClient**`: A convenience HTTP client for making payment-enabled requests | ||
|
|
||
| ## Compatibility | ||
|
|
||
| - Java 17+ | ||
| - Jakarta Servlet API or `javax.servlet` | ||
| - Works with any servlet container (Tomcat, Jetty, etc.) | ||
| - Compatible with Spring Boot, Quarkus, and other Java frameworks | ||
|
|
||
| ## Installation | ||
|
|
||
| To use this library, you need to build and install it locally: | ||
|
|
||
| ```bash | ||
| # Clone the repository | ||
| git clone https://github.com/coinbase/x402.git | ||
| cd x402/java | ||
|
|
||
| # Build and install to your local Maven repository | ||
| mvn clean install | ||
| ``` | ||
|
|
||
| Then add the dependency to your Maven project: | ||
|
|
||
| ```xml | ||
| <dependency> | ||
| <groupId>com.coinbase</groupId> | ||
| <artifactId>x402</artifactId> | ||
| <version>0.1.0-SNAPSHOT</version> | ||
| </dependency> | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Server Side - Requiring Payments for Access | ||
|
|
||
| Integrate the `x402` filter into your servlet-based application to require payment for specific paths: | ||
|
|
||
| ```java | ||
| import com.coinbase.x402.server.PaymentFilter; | ||
| import com.coinbase.x402.client.HttpFacilitatorClient; | ||
|
|
||
| import java.math.BigInteger; | ||
| import java.util.Map; | ||
|
|
||
| // 1. Define paths that require payment and their prices | ||
| Map<String, BigInteger> priceTable = Map.of( | ||
| "/api/premium", BigInteger.valueOf(1000), // 1000 wei | ||
| "/content/exclusive", BigInteger.valueOf(500) | ||
| ); | ||
|
|
||
| // 2. Create a facilitator client | ||
| String facilitatorUrl = "https://x402.org/faciliator"; | ||
| HttpFacilitatorClient facilitator = new HttpFacilitatorClient(facilitatorUrl); | ||
|
|
||
| // 3. Create and register the filter | ||
| String payToAddress = "0xYourReceiverAddress"; | ||
| PaymentFilter paymentFilter = new PaymentFilter(payToAddress, priceTable, facilitator); | ||
|
|
||
| // 4. Register the filter with your servlet container | ||
| // In a standard servlet app: | ||
| FilterRegistration.Dynamic registration = servletContext.addFilter("paymentFilter", paymentFilter); | ||
| registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); | ||
|
|
||
| // Or in Spring Boot: | ||
| @Bean | ||
| public FilterRegistration paymentFilter(ServletContext servletContext) { | ||
| FilterRegistration.Dynamic registration = servletContext.addFilter( | ||
| "paymentFilter", | ||
| new PaymentFilter(payToAddress, priceTable, new HttpFacilitatorClient(facilitatorUrl)) | ||
| ); | ||
| registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); | ||
| return registration; | ||
| } | ||
| ``` | ||
|
|
||
| ### Client Side - Making Requests with Payment | ||
|
|
||
| To make HTTP requests that include payment proofs: | ||
|
|
||
| ```java | ||
| import com.coinbase.x402.client.X402HttpClient; | ||
| import com.coinbase.x402.crypto.CryptoSigner; | ||
|
|
||
| import java.math.BigInteger; | ||
| import java.net.URI; | ||
| import java.net.http.HttpResponse; | ||
| import java.util.Map; | ||
|
|
||
| // 1. Implement the CryptoSigner interface with your crypto library | ||
| // This example uses a stub - you'd integrate with web3j, Solana-J, etc. | ||
| CryptoSigner signer = new CryptoSigner() { | ||
| @Override | ||
| public String sign(Map<String, Object> payload) { | ||
| // Sign the payload with your private key | ||
| return "0xYourSignatureHere"; | ||
| } | ||
| }; | ||
|
|
||
| // 2. Create the client | ||
| X402HttpClient client = new X402HttpClient(signer); | ||
|
|
||
| // 3. Make a GET request with payment | ||
| BigInteger amount = BigInteger.valueOf(1000); | ||
| String asset = "0xTokenContractAddress"; // Or "USDC", etc. | ||
| String payTo = "0xReceiverAddress"; | ||
| URI uri = URI.create("https://api.example.com/premium"); | ||
|
|
||
| HttpResponse<String> response = client.get(uri, amount, asset, payTo); | ||
| System.out.println("Response: " + response.body()); | ||
| ``` | ||
|
|
||
| ## Complete Example | ||
|
|
||
| Here's a complete example of a Spring Boot application with a paid joke API: | ||
|
|
||
| ```java | ||
| import com.coinbase.x402.server.PaymentFilter; | ||
| import com.coinbase.x402.client.HttpFacilitatorClient; | ||
| import javax.servlet.DispatcherType; | ||
| import javax.servlet.FilterRegistration; | ||
| import javax.servlet.ServletContext; | ||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
| import org.springframework.boot.web.servlet.ServletContextInitializer; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import java.math.BigInteger; | ||
| import java.util.EnumSet; | ||
| import java.util.Map; | ||
|
|
||
| @SpringBootApplication | ||
| public class PaidJokeApplication implements ServletContextInitializer { | ||
|
|
||
| public static void main(String[] args) { | ||
| SpringApplication.run(PaidJokeApplication.class, args); | ||
| } | ||
|
|
||
| @Override | ||
| public void onStartup(ServletContext servletContext) { | ||
| // Set up the payment filter | ||
| String facilitatorUrl = "https://x402.org/facilitator"; | ||
| HttpFacilitatorClient facilitator = new HttpFacilitatorClient(facilitatorUrl); | ||
|
|
||
| // Define which URLs require payment and their prices | ||
| Map<String, BigInteger> priceTable = Map.of( | ||
| "/api/joke", BigInteger.valueOf(1000) // 1000 wei for a premium joke | ||
| ); | ||
|
|
||
| // Create and register the filter | ||
| String payToAddress = "0xYourReceiverAddress"; | ||
| PaymentFilter paymentFilter = new PaymentFilter(payToAddress, priceTable, facilitator); | ||
|
|
||
| FilterRegistration.Dynamic registration = | ||
| servletContext.addFilter("paymentFilter", paymentFilter); | ||
| registration.addMappingForUrlPatterns( | ||
| EnumSet.of(DispatcherType.REQUEST), true, "/*"); | ||
| } | ||
|
|
||
| @RestController | ||
| static class JokeController { | ||
| @GetMapping("/api/joke") | ||
| public Map<String, String> getPremiumJoke() { | ||
| // If this code runs, payment was already verified by the filter | ||
| return Map.of("joke", "Why do programmers prefer dark mode? Because light attracts bugs!"); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. The server defines endpoints that require payment and their prices | ||
| 2. When a request comes in, the `PaymentFilter` checks if the path requires payment | ||
| 3. If payment is required, it looks for the `X-PAYMENT` header | ||
| 4. The facilitator verifies the payment and either approves or rejects the request | ||
| 5. If approved, the request continues; if rejected, a 402 Payment Required response is returned | ||
| 6. After serving the request, the filter calls the facilitator to settle the payment | ||
|
|
||
| ## Payment Flow | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant Client | ||
| participant Server | ||
| participant Facilitator | ||
|
|
||
| Client->>Server: GET /resource | ||
| Server->>Client: 402 Payment Required | ||
|
|
||
| Client->>Server: GET /resource (with X-PAYMENT header) | ||
| Server->>Facilitator: Verify payment | ||
| Facilitator->>Server: Payment verified | ||
|
|
||
| Server->>Client: 200 OK + content | ||
| Server->>Facilitator: Settle payment (async) | ||
| ``` | ||
|
|
||
| ## Error Handling | ||
|
|
||
| The PaymentFilter handles different types of errors with appropriate HTTP status codes: | ||
|
|
||
| ### Payment Required (402) | ||
| When a payment is required but not provided or is invalid, the filter returns a `402 Payment Required` response with a JSON body: | ||
|
|
||
| ```json | ||
| { | ||
| "x402Version": 1, | ||
| "accepts": [ | ||
| { | ||
| "scheme": "exact", | ||
| "network": "base-sepolia", | ||
| "maxAmountRequired": "1000", | ||
| "asset": "USDC", | ||
| "resource": "/api/premium", | ||
| "mimeType": "application/json", | ||
| "payTo": "0xReceiverAddress", | ||
| "maxTimeoutSeconds": 30 | ||
| } | ||
| ], | ||
| "error": "missing payment header" | ||
| } | ||
| ``` | ||
|
|
||
| ### Server Errors (500) | ||
| When the facilitator service is unavailable or other unexpected errors occur during payment verification: | ||
|
|
||
| ```json | ||
| { | ||
| "error": "Payment verification failed: Connection timeout" | ||
| } | ||
| ``` | ||
|
|
||
| Or for unexpected internal errors: | ||
|
|
||
| ```json | ||
| { | ||
| "error": "Internal server error during payment verification" | ||
| } | ||
| ``` | ||
|
|
||
| ### Settlement Errors | ||
| When payment settlement fails after successful verification, the filter returns a 402 status to prevent users from receiving content without proper payment completion. This matches the behavior of the Go and TypeScript implementations: | ||
|
|
||
| ```json | ||
| { | ||
| "x402Version": 1, | ||
| "accepts": [...], | ||
| "error": "settlement failed: insufficient balance" | ||
| } | ||
| ``` | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't do any error handling for non-402 cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated ✔️