OpenPay (1 crate, 3277 LOC, 18 tests)
op-ewallets-asia APAC e-wallet rail adapters. Alipay Cross-Border v3,
WeChat Pay (JSAPI/Native/H5/MicroPay) with constant-time
HMAC notify, UPI 2.0 (collect/intent/mandate/VPA),
Paytm, GrabPay, GoPay (Midtrans), Touch'n Go.
EMVCo MPM TLV + CRC-16/CCITT-FALSE encoder for
PromptPay (TH), PayNow (SG), DuitNow (MY), QRIS (ID).
Transport-free traits — operators inject HTTP + RSA-SHA256
signers (keeps POS firmware / WASM consumers reqwest-free).
Smart Byte (1 crate, 1310 LOC, 15 tests)
smart-byte-zk Selective-disclosure ZK primitives.
Real: Bulletproofs over Ristretto255 (range proofs at
8/16/32/64-bit), RangePredicate / InequalityPredicate /
SetMembershipPredicate all delegating to Bulletproofs.
Stubs (documented): Groth16, PLONK — deterministic
placeholder proofs satisfying ZkScheme trait for future
arkworks integration. AnonCred sketch + VerifiablePresentation
builder.
EOC (1 crate, ~3000 LOC, 36 tests)
eoc-rag RAG orchestrator. Naive (Lewis 2020), HyDE (Gao 2022),
RAG-Fusion + RRF (Cormack 2009), Self-RAG (Asai 2023)
with Retrieve/ISREL/ISSUP/ISUSE critique tokens,
Corrective RAG (Yan 2024) with retrieval evaluator
+ external fallback, Adaptive RAG (Jeong 2024)
complexity router. Chunking: sentence-window, semantic,
recursive, late-chunking. Span-backed citations with
3-mode enforcement. SelfCheckGPT consistency guard.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
142 lines
4.6 KiB
Rust
142 lines
4.6 KiB
Rust
//! WeChat Pay JSAPI happy-path + notify-callback HMAC verification.
|
|
|
|
use op_core::{Currency, Money};
|
|
use op_ewallets_asia::wallet::{
|
|
AsiaWallet, ChargeIntent, ChargeStatus, PresentmentMode,
|
|
};
|
|
use op_ewallets_asia::wechat::{
|
|
verify_notify_hmac, WeChatAdapter, WeChatSigner, WeChatTransport,
|
|
};
|
|
use std::sync::Mutex;
|
|
|
|
struct CannedTransport {
|
|
response: String,
|
|
seen: Mutex<Option<String>>,
|
|
}
|
|
|
|
impl WeChatTransport for CannedTransport {
|
|
fn post_json(
|
|
&self,
|
|
path: &str,
|
|
_headers: &[(String, String)],
|
|
_body: &str,
|
|
) -> op_ewallets_asia::Result<String> {
|
|
*self.seen.lock().unwrap() = Some(path.into());
|
|
Ok(self.response.clone())
|
|
}
|
|
}
|
|
|
|
struct StubSigner;
|
|
impl WeChatSigner for StubSigner {
|
|
fn sign(&self, _canonical: &[u8]) -> op_ewallets_asia::Result<String> {
|
|
Ok("sig".into())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn jsapi_happy_path() {
|
|
let canned = r#"{ "prepay_id": "wx20211111000000abcdef" }"#;
|
|
let adapter = WeChatAdapter {
|
|
mch_id: "1900000109".into(),
|
|
app_id: "wxd678efh567hg6787".into(),
|
|
host: "https://api.mch.weixin.qq.com".into(),
|
|
transport: Box::new(CannedTransport {
|
|
response: canned.into(),
|
|
seen: Mutex::new(None),
|
|
}),
|
|
signer: Box::new(StubSigner),
|
|
};
|
|
let intent = ChargeIntent {
|
|
merchant_order_id: "ord-77".into(),
|
|
amount: Money::from_minor(1_500, Currency::CNY),
|
|
description: "OpenPay test".into(),
|
|
presentment: PresentmentMode::InAppJsApi,
|
|
consumer_hint: Some("ozt0n5UqEEFvxOQzm9fHsKVklyAU".into()),
|
|
notify_url: Some("https://merchant.example/notify".into()),
|
|
};
|
|
let result = adapter.create_charge(&intent).expect("wechat jsapi");
|
|
assert_eq!(result.status, ChargeStatus::Pending);
|
|
assert_eq!(result.presentment_payload, "wx20211111000000abcdef");
|
|
}
|
|
|
|
#[test]
|
|
fn jsapi_rejects_missing_openid() {
|
|
let adapter = WeChatAdapter {
|
|
mch_id: "x".into(),
|
|
app_id: "x".into(),
|
|
host: "https://api.mch.weixin.qq.com".into(),
|
|
transport: Box::new(CannedTransport {
|
|
response: "{}".into(),
|
|
seen: Mutex::new(None),
|
|
}),
|
|
signer: Box::new(StubSigner),
|
|
};
|
|
let intent = ChargeIntent {
|
|
merchant_order_id: "o".into(),
|
|
amount: Money::from_minor(100, Currency::CNY),
|
|
description: "x".into(),
|
|
presentment: PresentmentMode::InAppJsApi,
|
|
consumer_hint: None,
|
|
notify_url: Some("https://merchant.example/notify".into()),
|
|
};
|
|
assert!(adapter.create_charge(&intent).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn notify_hmac_roundtrip() {
|
|
use hmac::{Hmac, Mac};
|
|
use sha2::Sha256;
|
|
|
|
let secret = b"shared-notify-secret";
|
|
let timestamp = "1670000000";
|
|
let nonce = "abc123";
|
|
let body = r#"{"event_type":"TRANSACTION.SUCCESS"}"#;
|
|
|
|
// Compute expected the same way the verifier expects.
|
|
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(secret).unwrap();
|
|
mac.update(timestamp.as_bytes());
|
|
mac.update(b"\n");
|
|
mac.update(nonce.as_bytes());
|
|
mac.update(b"\n");
|
|
mac.update(body.as_bytes());
|
|
mac.update(b"\n");
|
|
let tag = mac.finalize().into_bytes();
|
|
|
|
// Standard base64 encode.
|
|
let b64 = base64_encode(&tag);
|
|
assert!(verify_notify_hmac(timestamp, nonce, body, &b64, secret).is_ok());
|
|
|
|
// Tamper with the body — should fail.
|
|
let bad = verify_notify_hmac(timestamp, nonce, "{}", &b64, secret);
|
|
assert!(bad.is_err());
|
|
}
|
|
|
|
fn base64_encode(data: &[u8]) -> String {
|
|
const ALPHA: &[u8; 64] =
|
|
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
let mut out = String::new();
|
|
let mut i = 0;
|
|
while i + 3 <= data.len() {
|
|
let n = (u32::from(data[i]) << 16) | (u32::from(data[i + 1]) << 8) | u32::from(data[i + 2]);
|
|
out.push(ALPHA[((n >> 18) & 0x3F) as usize] as char);
|
|
out.push(ALPHA[((n >> 12) & 0x3F) as usize] as char);
|
|
out.push(ALPHA[((n >> 6) & 0x3F) as usize] as char);
|
|
out.push(ALPHA[(n & 0x3F) as usize] as char);
|
|
i += 3;
|
|
}
|
|
let rem = data.len() - i;
|
|
if rem == 1 {
|
|
let n = u32::from(data[i]) << 16;
|
|
out.push(ALPHA[((n >> 18) & 0x3F) as usize] as char);
|
|
out.push(ALPHA[((n >> 12) & 0x3F) as usize] as char);
|
|
out.push('=');
|
|
out.push('=');
|
|
} else if rem == 2 {
|
|
let n = (u32::from(data[i]) << 16) | (u32::from(data[i + 1]) << 8);
|
|
out.push(ALPHA[((n >> 18) & 0x3F) as usize] as char);
|
|
out.push(ALPHA[((n >> 12) & 0x3F) as usize] as char);
|
|
out.push(ALPHA[((n >> 6) & 0x3F) as usize] as char);
|
|
out.push('=');
|
|
}
|
|
out
|
|
}
|