Skip to content

Commit e408988

Browse files
jtang17claude
andcommitted
feat: Make Aptos transaction sponsorship optional
Add optional gas sponsorship for Aptos transactions, allowing facilitators to choose between sponsored mode (facilitator pays gas) and non-sponsored mode (client pays own gas). Changes: - Make AptosChainConfig.signer optional, add sponsor_gas boolean flag - Update AptosChainProvider to handle optional fee payer configuration - Update /supported endpoint to include extra.sponsored when enabled - Update settle flow to conditionally sponsor based on configuration - Add validation: signer required when sponsor_gas is true Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6b845c3 commit e408988

File tree

3 files changed

+140
-68
lines changed

3 files changed

+140
-68
lines changed

src/chain/aptos.rs

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,17 @@ impl From<AptosChainProviderError> for X402SchemeFacilitatorError {
137137

138138
pub struct AptosChainProvider {
139139
chain: AptosChainReference,
140-
account_address: AccountAddress,
141-
private_key: Ed25519PrivateKey,
140+
sponsor_gas: bool,
141+
fee_payer_address: Option<AccountAddress>,
142+
fee_payer_private_key: Option<Ed25519PrivateKey>,
142143
rest_client: Arc<AptosClient>,
143144
}
144145

145146
impl Debug for AptosChainProvider {
146147
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147148
f.debug_struct("AptosChainProvider")
148149
.field("chain", &self.chain)
150+
.field("sponsor_gas", &self.sponsor_gas)
149151
.field("rpc_url", &"<rest_client>")
150152
.finish()
151153
}
@@ -157,44 +159,68 @@ impl AptosChainProvider {
157159
) -> Result<Self, Box<dyn std::error::Error>> {
158160
let chain = config.chain_reference();
159161
let rpc_url = config.rpc();
162+
let sponsor_gas = config.sponsor_gas();
160163

161-
// Parse private key (hex with or without 0x prefix)
162-
let private_key_hex = config.signer().to_string();
163-
let private_key_hex = private_key_hex.trim_start_matches("0x");
164-
let private_key_bytes = hex::decode(private_key_hex)?;
165-
let private_key = Ed25519PrivateKey::try_from(private_key_bytes.as_slice())?;
164+
// Validate: if sponsoring, signer must be provided
165+
if sponsor_gas && config.signer().is_none() {
166+
return Err("signer configuration required when sponsor_gas is true".into());
167+
}
168+
169+
// Parse private key if signer is provided
170+
let (fee_payer_address, fee_payer_private_key) = if let Some(signer) = config.signer() {
171+
let private_key_hex = signer.to_string();
172+
let private_key_hex = private_key_hex.trim_start_matches("0x");
173+
let private_key_bytes = hex::decode(private_key_hex)?;
174+
let private_key = Ed25519PrivateKey::try_from(private_key_bytes.as_slice())?;
175+
176+
// Derive account address from public key
177+
use aptos_crypto::ed25519::Ed25519PublicKey;
178+
use aptos_types::transaction::authenticator::AuthenticationKey;
179+
180+
let public_key: Ed25519PublicKey = (&private_key).into();
181+
let auth_key = AuthenticationKey::ed25519(&public_key);
182+
let account_address = auth_key.account_address();
183+
184+
(Some(account_address), Some(private_key))
185+
} else {
186+
(None, None)
187+
};
166188

167189
let rest_client = AptosClient::new(rpc_url.clone());
168190

169-
let provider = Self::new(chain, private_key, rest_client);
191+
let provider = Self::new(chain, sponsor_gas, fee_payer_address, fee_payer_private_key, rest_client);
170192
Ok(provider)
171193
}
172194

173195
pub fn new(
174196
chain: AptosChainReference,
175-
private_key: Ed25519PrivateKey,
197+
sponsor_gas: bool,
198+
fee_payer_address: Option<AccountAddress>,
199+
fee_payer_private_key: Option<Ed25519PrivateKey>,
176200
rest_client: AptosClient,
177201
) -> Self {
178-
// Derive account address from public key
179-
use aptos_crypto::ed25519::Ed25519PublicKey;
180-
use aptos_types::transaction::authenticator::AuthenticationKey;
181-
182-
let public_key: Ed25519PublicKey = (&private_key).into();
183-
let auth_key = AuthenticationKey::ed25519(&public_key);
184-
let account_address = auth_key.account_address();
185-
186202
{
187203
let chain_id: ChainId = chain.into();
188-
tracing::info!(
189-
chain = %chain_id,
190-
address = %account_address,
191-
"Initialized Aptos provider"
192-
);
204+
if let Some(address) = fee_payer_address {
205+
tracing::info!(
206+
chain = %chain_id,
207+
address = %address,
208+
sponsor_gas = sponsor_gas,
209+
"Initialized Aptos provider with fee payer"
210+
);
211+
} else {
212+
tracing::info!(
213+
chain = %chain_id,
214+
sponsor_gas = sponsor_gas,
215+
"Initialized Aptos provider without fee payer"
216+
);
217+
}
193218
}
194219
Self {
195220
chain,
196-
account_address,
197-
private_key,
221+
sponsor_gas,
222+
fee_payer_address,
223+
fee_payer_private_key,
198224
rest_client: Arc::new(rest_client),
199225
}
200226
}
@@ -203,18 +229,26 @@ impl AptosChainProvider {
203229
&self.rest_client
204230
}
205231

206-
pub fn account_address(&self) -> AccountAddress {
207-
self.account_address
232+
pub fn sponsor_gas(&self) -> bool {
233+
self.sponsor_gas
208234
}
209235

210-
pub fn private_key(&self) -> &Ed25519PrivateKey {
211-
&self.private_key
236+
pub fn account_address(&self) -> Option<AccountAddress> {
237+
self.fee_payer_address
238+
}
239+
240+
pub fn private_key(&self) -> Option<&Ed25519PrivateKey> {
241+
self.fee_payer_private_key.as_ref()
212242
}
213243
}
214244

215245
impl ChainProviderOps for AptosChainProvider {
216246
fn signer_addresses(&self) -> Vec<String> {
217-
vec![Address::new(self.account_address).to_string()]
247+
if let Some(address) = self.fee_payer_address {
248+
vec![Address::new(address).to_string()]
249+
} else {
250+
vec![]
251+
}
218252
}
219253

220254
fn chain_id(&self) -> ChainId {

src/config.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -563,12 +563,15 @@ pub struct AptosChainConfig {
563563
}
564564

565565
impl AptosChainConfig {
566-
pub fn signer(&self) -> &AptosSignerConfig {
567-
&self.inner.signer
566+
pub fn signer(&self) -> Option<&AptosSignerConfig> {
567+
self.inner.signer.as_ref()
568568
}
569569
pub fn rpc(&self) -> &Url {
570570
&self.inner.rpc
571571
}
572+
pub fn sponsor_gas(&self) -> bool {
573+
self.inner.sponsor_gas
574+
}
572575
pub fn chain_reference(&self) -> aptos::AptosChainReference {
573576
self.chain_reference
574577
}
@@ -580,11 +583,16 @@ impl AptosChainConfig {
580583
/// Configuration specific to Aptos chains.
581584
#[derive(Debug, Clone, Serialize, Deserialize)]
582585
pub struct AptosChainConfigInner {
583-
/// Signer configuration for this chain (required).
584-
/// A hex-encoded private key (32 or 64 bytes) or env var reference.
585-
pub signer: AptosSignerConfig,
586586
/// RPC provider URL for this chain (required).
587587
pub rpc: Url,
588+
/// Signer configuration for this chain (optional, required only if sponsor_gas is true).
589+
/// A hex-encoded private key (32 or 64 bytes) or env var reference.
590+
#[serde(default, skip_serializing_if = "Option::is_none")]
591+
pub signer: Option<AptosSignerConfig>,
592+
/// Whether the facilitator should sponsor gas fees for transactions (default: false).
593+
/// If true, facilitator signs as fee payer and pays gas. If false, users pay their own gas.
594+
#[serde(default)]
595+
pub sponsor_gas: bool,
588596
}
589597

590598
/// Configuration for chains.

src/scheme/v2_aptos_exact/mod.rs

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,19 @@ impl X402SchemeFacilitator for V2AptosExactFacilitator {
108108

109109
async fn supported(&self) -> Result<proto::SupportedResponse, X402SchemeFacilitatorError> {
110110
let chain_id = self.provider.chain_id();
111+
112+
// Include extra.sponsored if the facilitator is configured to sponsor gas
113+
let extra = if self.provider.sponsor_gas() {
114+
Some(serde_json::json!({ "sponsored": true }))
115+
} else {
116+
None
117+
};
118+
111119
let kinds: Vec<proto::SupportedPaymentKind> = vec![proto::SupportedPaymentKind {
112120
x402_version: proto::v2::X402Version2.into(),
113121
scheme: ExactScheme.to_string(),
114122
network: chain_id.to_string(),
115-
extra: None,
123+
extra,
116124
}];
117125
let signers = {
118126
let mut signers = HashMap::with_capacity(1);
@@ -217,41 +225,63 @@ pub async fn settle_transaction(
217225
))
218226
})?;
219227

220-
// For sponsored transactions, sign as fee payer
221-
let fee_payer_address = provider.account_address();
222-
let fee_payer_private_key = provider.private_key();
223-
let fee_payer_public_key: Ed25519PublicKey = fee_payer_private_key.into();
224-
225-
// Create the message that the fee payer needs to sign
226-
// This is the same message the sender signed: RawTransactionWithData::new_fee_payer
227-
let fee_payer_message = RawTransactionWithData::new_fee_payer(
228-
verification.raw_transaction.clone(),
229-
vec![], // No secondary signers
230-
fee_payer_address,
231-
);
232-
233-
// Sign as fee payer - sign the message directly (it implements CryptoHash)
234-
let fee_payer_signature = fee_payer_private_key
235-
.sign(&fee_payer_message)
236-
.map_err(|e| {
237-
PaymentVerificationError::InvalidSignature(format!(
238-
"Failed to sign as fee payer: {}",
239-
e
240-
))
228+
let signed_txn = if provider.sponsor_gas() {
229+
// Sponsored transaction: facilitator signs as fee payer
230+
let fee_payer_address = provider.account_address().ok_or_else(|| {
231+
PaymentVerificationError::InvalidFormat(
232+
"Fee payer address not configured for sponsored transaction".to_string(),
233+
)
234+
})?;
235+
let fee_payer_private_key = provider.private_key().ok_or_else(|| {
236+
PaymentVerificationError::InvalidFormat(
237+
"Fee payer private key not configured for sponsored transaction".to_string(),
238+
)
241239
})?;
240+
let fee_payer_public_key: Ed25519PublicKey = fee_payer_private_key.into();
241+
242+
// Create the message that the fee payer needs to sign
243+
let fee_payer_message = RawTransactionWithData::new_fee_payer(
244+
verification.raw_transaction.clone(),
245+
vec![], // No secondary signers
246+
fee_payer_address,
247+
);
248+
249+
// Sign as fee payer
250+
let fee_payer_signature = fee_payer_private_key
251+
.sign(&fee_payer_message)
252+
.map_err(|e| {
253+
PaymentVerificationError::InvalidSignature(format!(
254+
"Failed to sign as fee payer: {}",
255+
e
256+
))
257+
})?;
258+
259+
let fee_payer_authenticator =
260+
AccountAuthenticator::ed25519(fee_payer_public_key.clone(), fee_payer_signature);
261+
262+
// Create fee payer signed transaction
263+
SignedTransaction::new_fee_payer(
264+
verification.raw_transaction.clone(),
265+
sender_authenticator,
266+
vec![], // No secondary signer addresses
267+
vec![], // No secondary signers
268+
fee_payer_address,
269+
fee_payer_authenticator,
270+
)
271+
} else {
272+
// Non-sponsored transaction: client pays own gas, just submit their fully-signed transaction
273+
// Extract public key and signature from the sender's authenticator
274+
let (public_key, signature) = match sender_authenticator {
275+
AccountAuthenticator::Ed25519 { public_key, signature } => (public_key, signature),
276+
_ => {
277+
return Err(PaymentVerificationError::InvalidFormat(
278+
"Only Ed25519 signatures are supported for non-sponsored transactions".to_string(),
279+
));
280+
}
281+
};
242282

243-
let fee_payer_authenticator =
244-
AccountAuthenticator::ed25519(fee_payer_public_key.clone(), fee_payer_signature);
245-
246-
// Create fee payer signed transaction
247-
let signed_txn = SignedTransaction::new_fee_payer(
248-
verification.raw_transaction.clone(),
249-
sender_authenticator,
250-
vec![], // No secondary signer addresses
251-
vec![], // No secondary signers
252-
fee_payer_address,
253-
fee_payer_authenticator,
254-
);
283+
SignedTransaction::new(verification.raw_transaction.clone(), public_key, signature)
284+
};
255285

256286
// Compute transaction hash after signing
257287
let tx_hash = signed_txn.committed_hash();

0 commit comments

Comments
 (0)