|
| 1 | +package com.coinbase.cdp.examples.solana; |
| 2 | + |
| 3 | +import com.coinbase.cdp.CdpClient; |
| 4 | +import com.coinbase.cdp.examples.utils.EnvLoader; |
| 5 | +import com.coinbase.cdp.openapi.api.X402FacilitatorApi; |
| 6 | +import com.coinbase.cdp.openapi.model.CreateSolanaAccountRequest; |
| 7 | +import com.coinbase.cdp.openapi.model.InlineObject; |
| 8 | +import com.coinbase.cdp.openapi.model.InlineObject2; |
| 9 | +import com.coinbase.cdp.openapi.model.SignSolanaTransaction200Response; |
| 10 | +import com.coinbase.cdp.openapi.model.SignSolanaTransactionRequest; |
| 11 | +import com.coinbase.cdp.openapi.model.SolanaAccount; |
| 12 | +import com.coinbase.cdp.openapi.model.VerifyX402PaymentRequest; |
| 13 | +import com.coinbase.cdp.openapi.model.X402ExactSolanaPayload; |
| 14 | +import com.coinbase.cdp.openapi.model.X402PaymentPayload; |
| 15 | +import com.coinbase.cdp.openapi.model.X402PaymentRequirements; |
| 16 | +import com.coinbase.cdp.openapi.model.X402SupportedPaymentKind; |
| 17 | +import com.coinbase.cdp.openapi.model.X402V1PaymentPayload; |
| 18 | +import com.coinbase.cdp.openapi.model.X402V1PaymentPayloadPayload; |
| 19 | +import com.coinbase.cdp.openapi.model.X402V1PaymentRequirements; |
| 20 | +import com.coinbase.cdp.openapi.model.X402Version; |
| 21 | +import com.coinbase.cdp.utils.SolanaTransactionBuilder; |
| 22 | +import java.math.BigInteger; |
| 23 | +import org.p2p.solanaj.core.PublicKey; |
| 24 | +import org.p2p.solanaj.rpc.RpcClient; |
| 25 | + |
| 26 | +/** |
| 27 | + * Demonstrates the x402 payment flow on Solana using the CDP SDK. |
| 28 | + * |
| 29 | + * <p>x402 is a protocol for HTTP-native payments. The payer constructs and signs a transaction but |
| 30 | + * does NOT broadcast it directly. Instead, the signed transaction is included as a payment payload |
| 31 | + * in HTTP headers. The x402 facilitator verifies and settles the transaction on behalf of the |
| 32 | + * resource provider. |
| 33 | + * |
| 34 | + * <p>This example shows the full flow: |
| 35 | + * |
| 36 | + * <ol> |
| 37 | + * <li>Query supported x402 payment kinds from the facilitator |
| 38 | + * <li>Create a CDP-managed Solana account (the payer) |
| 39 | + * <li>Construct an unsigned SOL transfer transaction via the SDK's SolanaTransactionBuilder |
| 40 | + * <li>Sign the transaction via CDP (sign-only, no broadcast) |
| 41 | + * <li>Assemble the x402 payment payload and verify it with the facilitator |
| 42 | + * </ol> |
| 43 | + * |
| 44 | + * <p>Note: This example does not fund the account via faucet. The verify call will likely return |
| 45 | + * invalid due to insufficient funds, but the full API flow is demonstrated. In production, the |
| 46 | + * payer account would be funded before constructing the payment transaction. |
| 47 | + */ |
| 48 | +public class SignTransactionX402 { |
| 49 | + |
| 50 | + private static final String SOLANA_DEVNET_RPC = "https://api.devnet.solana.com"; |
| 51 | + |
| 52 | + // Recipient address representing the x402 payee (resource provider or facilitator). |
| 53 | + private static final String PAYEE_ADDRESS = "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE"; |
| 54 | + |
| 55 | + // Transfer amount in lamports (0.0001 SOL). |
| 56 | + private static final BigInteger TRANSFER_LAMPORTS = BigInteger.valueOf(100_000); |
| 57 | + |
| 58 | + public static void main(String[] args) throws Exception { |
| 59 | + EnvLoader.load(); |
| 60 | + |
| 61 | + try (CdpClient cdp = CdpClient.create()) { |
| 62 | + |
| 63 | + // --------------------------------------------------------------- |
| 64 | + // Step 1: Query supported x402 payment kinds |
| 65 | + // --------------------------------------------------------------- |
| 66 | + System.out.println("=== Step 1: Query supported x402 payment kinds ==="); |
| 67 | + X402FacilitatorApi x402Api = new X402FacilitatorApi(cdp.getApiClient()); |
| 68 | + InlineObject2 supported = x402Api.supportedX402PaymentKinds(); |
| 69 | + |
| 70 | + System.out.println("Supported payment kinds:"); |
| 71 | + for (X402SupportedPaymentKind kind : supported.getKinds()) { |
| 72 | + System.out.printf( |
| 73 | + " - version=%s, scheme=%s, network=%s%n", |
| 74 | + kind.getX402Version(), kind.getScheme(), kind.getNetwork()); |
| 75 | + } |
| 76 | + System.out.println("Signer networks: " + supported.getSigners().keySet()); |
| 77 | + System.out.println(); |
| 78 | + |
| 79 | + // --------------------------------------------------------------- |
| 80 | + // Step 2: Create a CDP-managed Solana account (the payer) |
| 81 | + // --------------------------------------------------------------- |
| 82 | + System.out.println("=== Step 2: Create a Solana account ==="); |
| 83 | + SolanaAccount account = |
| 84 | + cdp.solana().createAccount(new CreateSolanaAccountRequest().name("x402-payer-example")); |
| 85 | + String payerAddress = account.getAddress(); |
| 86 | + System.out.println("Payer address: " + payerAddress); |
| 87 | + System.out.println(); |
| 88 | + |
| 89 | + // --------------------------------------------------------------- |
| 90 | + // Step 3: Build an unsigned SOL transfer transaction |
| 91 | + // --------------------------------------------------------------- |
| 92 | + // In an x402 flow, this transaction represents the payment to the |
| 93 | + // resource provider. The amount and recipient would come from the |
| 94 | + // server's 402 Payment Required response (X-Payment header). |
| 95 | + System.out.println("=== Step 3: Build unsigned Solana transfer transaction ==="); |
| 96 | + RpcClient rpcClient = new RpcClient(SOLANA_DEVNET_RPC); |
| 97 | + PublicKey fromKey = new PublicKey(payerAddress); |
| 98 | + PublicKey toKey = new PublicKey(PAYEE_ADDRESS); |
| 99 | + |
| 100 | + String unsignedTxBase64 = |
| 101 | + SolanaTransactionBuilder.buildNativeTransfer(rpcClient, fromKey, toKey, TRANSFER_LAMPORTS); |
| 102 | + |
| 103 | + System.out.println("Unsigned transaction (base64): " + unsignedTxBase64.substring(0, 40) + "..."); |
| 104 | + System.out.println(); |
| 105 | + |
| 106 | + // --------------------------------------------------------------- |
| 107 | + // Step 4: Sign the transaction via CDP (sign-only, no broadcast) |
| 108 | + // --------------------------------------------------------------- |
| 109 | + // The key x402 distinction: we sign but do NOT send. The signed |
| 110 | + // transaction becomes the payment payload that accompanies the |
| 111 | + // HTTP request to the paid resource. The x402 facilitator settles |
| 112 | + // it after verifying the payment. |
| 113 | + System.out.println("=== Step 4: Sign transaction via CDP ==="); |
| 114 | + SignSolanaTransaction200Response signResponse = |
| 115 | + cdp.solana() |
| 116 | + .signTransaction( |
| 117 | + payerAddress, |
| 118 | + new SignSolanaTransactionRequest().transaction(unsignedTxBase64)); |
| 119 | + |
| 120 | + String signedTxBase64 = signResponse.getSignedTransaction(); |
| 121 | + System.out.println("Signed transaction (base64): " + signedTxBase64.substring(0, 40) + "..."); |
| 122 | + System.out.println(); |
| 123 | + |
| 124 | + // --------------------------------------------------------------- |
| 125 | + // Step 5: Assemble the x402 payment payload and verify |
| 126 | + // --------------------------------------------------------------- |
| 127 | + // In a real x402 flow: |
| 128 | + // 1. Client requests a paid resource and receives 402 + payment requirements |
| 129 | + // 2. Client constructs and signs the payment transaction (steps 3-4 above) |
| 130 | + // 3. Client retries the request with the signed tx in the X-Payment header |
| 131 | + // 4. The server (resource provider) calls verify/settle via the facilitator |
| 132 | + // |
| 133 | + // Here we call verify directly to demonstrate the facilitator API. |
| 134 | + System.out.println("=== Step 5: Verify x402 payment ==="); |
| 135 | + |
| 136 | + // Wrap the signed transaction in the Solana-specific payload |
| 137 | + X402ExactSolanaPayload solanaPayload = |
| 138 | + new X402ExactSolanaPayload().transaction(signedTxBase64); |
| 139 | + |
| 140 | + // Build the v1 payment payload |
| 141 | + X402V1PaymentPayload paymentPayload = |
| 142 | + new X402V1PaymentPayload() |
| 143 | + .x402Version(X402Version.NUMBER_1) |
| 144 | + .scheme(X402V1PaymentPayload.SchemeEnum.EXACT) |
| 145 | + .network(X402V1PaymentPayload.NetworkEnum.SOLANA_DEVNET) |
| 146 | + .payload(new X402V1PaymentPayloadPayload(solanaPayload)); |
| 147 | + |
| 148 | + // Build the payment requirements (mirroring what a server would provide) |
| 149 | + X402V1PaymentRequirements paymentRequirements = |
| 150 | + new X402V1PaymentRequirements() |
| 151 | + .scheme(X402V1PaymentRequirements.SchemeEnum.EXACT) |
| 152 | + .network(X402V1PaymentRequirements.NetworkEnum.SOLANA_DEVNET) |
| 153 | + .maxAmountRequired(TRANSFER_LAMPORTS.toString()) |
| 154 | + .resource("https://example.com/api/paid-resource") |
| 155 | + .description("Example x402 Solana payment") |
| 156 | + .mimeType("application/json") |
| 157 | + .payTo(PAYEE_ADDRESS) |
| 158 | + .maxTimeoutSeconds(300) |
| 159 | + .asset("sol"); |
| 160 | + |
| 161 | + // Verify the payment with the x402 facilitator |
| 162 | + InlineObject verifyResponse = |
| 163 | + x402Api.verifyX402Payment( |
| 164 | + new VerifyX402PaymentRequest() |
| 165 | + .x402Version(X402Version.NUMBER_1) |
| 166 | + .paymentPayload(new X402PaymentPayload(paymentPayload)) |
| 167 | + .paymentRequirements(new X402PaymentRequirements(paymentRequirements))); |
| 168 | + |
| 169 | + System.out.println("Verification result:"); |
| 170 | + System.out.println(" Valid: " + verifyResponse.getIsValid()); |
| 171 | + System.out.println(" Payer: " + verifyResponse.getPayer()); |
| 172 | + if (!verifyResponse.getIsValid()) { |
| 173 | + System.out.println(" Reason: " + verifyResponse.getInvalidReason()); |
| 174 | + System.out.println(" Message: " + verifyResponse.getInvalidMessage()); |
| 175 | + System.out.println(); |
| 176 | + System.out.println( |
| 177 | + "Note: The payment is expected to be invalid in this example because the"); |
| 178 | + System.out.println( |
| 179 | + "account was not funded. In production, the payer account would have"); |
| 180 | + System.out.println("sufficient SOL before constructing the payment transaction."); |
| 181 | + } |
| 182 | + System.out.println(); |
| 183 | + System.out.println("x402 Solana sign transaction example complete."); |
| 184 | + } |
| 185 | + } |
| 186 | +} |
0 commit comments