Sign in

    moon-pay-sdk

    Native MoonBit SDK for WeChat Pay and Alipay

    payment
    wechat-pay
    alipay
    sdk
    Download zip
    Author
    Version
    0.10.0
    License
    MIT
    Last updated
    15 days ago
    Downloads
    29

    #moon-pay-sdk

    Native MoonBit SDK for WeChat Pay and Alipay.

    moon-pay-sdk is a server-side payment SDK written in native MoonBit. It lets MoonBit applications integrate with WeChat Pay and Alipay: create orders, query their status, and handle the asynchronous payment notifications both providers push to your server.

    The project is being developed for the MoonBit Hackathon. It targets the MoonBit native backend — no JavaScript bridge, no interpreter in the loop. The whole payment flow, from RSA request signing to TLS transport, runs as native code built on moonbitlang/async and the pure-MoonBit cryptography of moonbitstack/mooncrypt.

    Status: v0.1.0 implements everything under Features; the code samples compile against the shipped API.

    #Features

    #WeChat Pay (API v3)

    • Native payment — create an order and get a code_url to render as a QR code on your website
    • H5 payment — create an order and get a URL that opens WeChat Pay in a mobile browser
    • JSAPI payment — create an in-WeChat order (mini-program or official-account page) with the payer's openid, and build the signed wx.requestPayment parameter set
    • Mini-program payment — a dedicated miniprogram package: exchange a wx.login code for the payer's openid (code2session) and produce the signed wx.requestPayment parameter set in one call
    • Order query — look up an order by out-trade number or transaction ID, to reconcile missed callbacks
    • Callback handling — verify the callback signature against platform certificates, decrypt the AES-256-GCM payload, and parse the payment result
    • Public-key mode (公钥模式) — alternatively verify WeChat's signatures with the console-issued WeChat Pay public key (PUB_KEY_ID_...) instead of downloaded platform certificates

    #Alipay (OpenAPI)

    • Page payment — alipay.trade.page.pay, redirect URL for desktop website checkout
    • Wap payment — alipay.trade.wap.pay, for mobile website checkout
    • Order query — alipay.trade.query
    • Async notification handling — verify RSA2 signatures on the form parameters Alipay posts to your notify_url

    #Not in scope (yet)

    Refunds, transfers, bill download and sub-merchant APIs are left for later releases — see Roadmap.

    #Requirements

    • MoonBit toolchain (moon) with the native backend
    • Linux (epoll) or macOS (kqueue)
    • OpenSSL — loaded at runtime by moonbitlang/async for TLS transport
    • A WeChat Pay merchant account (API v3 certificates and keys) and/or an Alipay open-platform application, to actually move money

    #Installation

    moon add daqing/moon-pay-sdk

    Then import the packages you need in your moon.pkg:

    import { "daqing/moon-pay-sdk/wechat", "daqing/moon-pay-sdk/alipay", "daqing/moon-pay-sdk/miniprogram", }

    #Usage

    #WeChat Pay: QR checkout and order query

    ///|
    async fn main {
    let wechat = @wechat.Client::new(
    config=@wechat.Config::new(
    appid="wx8888888888888888",
    mchid="1900000000",
    serial_no="YOUR-CERT-SERIAL",
    private_key_pem~, // PEM text, loaded once at startup
    api_v3_key="YOUR-32-CHARACTER-APIV3-KEY",
    ),
    transport=@transport.HttpClient::new(),
    )

    // Native payment: render order.code_url() as a QR code.
    let order = wechat.native_order(
    out_trade_no="hackathon-20261001-0001",
    total=100, // in fen: 100 = CNY 1.00
    description="MoonBit Hackathon Ticket",
    notify_url="https://example.com/callback/wechat",
    )
    println("QR content: \{order.code_url()}")

    // JSAPI payment (mini-program / in-WeChat page): pass the payer's openid
    // and hand the signed parameter set to wx.requestPayment.
    let jsapi = wechat.jsapi_order(
    out_trade_no="hackathon-20261001-0002",
    total=100,
    description="MoonBit Hackathon Ticket",
    notify_url="https://example.com/callback/wechat",
    openid="oUpF8uMuAJO_M2pxb1Q9zNjWeS6o",
    )
    let pay = wechat.jsapi_pay_params(prepay_id=jsapi.prepay_id())
    println("wx.requestPayment timeStamp: \{pay.timestamp()}")

    // Callbacks can get lost — query to reconcile.
    let paid = wechat.query_order(out_trade_no="hackathon-20261001-0001")
    if paid.trade_state() is @wechat.Success {
    println("paid \{paid.total_fen()} fen")
    }
    }

    #WeChat Pay: mini-program checkout

    ///|
    async fn main {
    let wechat = @wechat.Client::new(
    config=@wechat.Config::new(
    appid="wx8888888888888888", // the mini-program appid
    mchid="1900000000",
    serial_no="YOUR-CERT-SERIAL",
    private_key_pem~, // PEM text, loaded once at startup
    api_v3_key="YOUR-32-CHARACTER-APIV3-KEY",
    ),
    transport=@transport.HttpClient::new(),
    )
    let miniapp = @miniprogram.Client::new(
    config=@miniprogram.Config::new(
    app_id="wx8888888888888888", // must equal the pay client's appid
    app_secret="YOUR-MINI-PROGRAM-APP-SECRET",
    ),
    pay=wechat,
    transport=@transport.HttpClient::new(),
    )
    // One call from the wx.login code to wx.requestPayment parameters; codes
    // are single-use and expire in five minutes, so exchange at order time.
    let result = miniapp.pay(
    out_trade_no="hackathon-20261001-0006",
    total=100,
    description="MoonBit Hackathon Ticket",
    notify_url="https://example.com/callback/wechat",
    js_code~, // fresh from wx.login
    )
    // Return result.pay_params() to the mini-program for wx.requestPayment.
    }

    #WeChat Pay: callback

    ///|
    async fn handle_wechat_callback(
    wechat : @wechat.Client,
    headers : Array[(String, String)],
    body : Bytes,
    ) -> (Int, String) {
    // Verifies the WeChat Pay signature (replay window included), decrypts the
    // AES-256-GCM resource and returns the parsed payment result.
    let notification = wechat.verify_callback(headers, body)
    if notification.trade_state() is wechat.Success {
    // notification.out_trade_no() is paid — update your own order storage.
    @wechat.ack_success()
    } else {
    @wechat.ack_failure(message="processing failed")
    }
    }

    #Alipay: page / wap checkout

    ///|
    async fn main {
    let alipay = @alipay.Client::new(
    config=@alipay.Config::new(
    app_id="2021000000000000",
    private_key_pem~, // your application private key, PEM
    alipay_public_key_pem~, // for verifying notifications
    ),
    transport=@transport.HttpClient::new(),
    )

    // Desktop website checkout: redirect the customer to this URL.
    let url = alipay.page_pay_url(
    out_trade_no="hackathon-20261001-0002",
    total_fen=8800, // 8800 fen = CNY 88.00
    subject="MoonBit Hackathon Ticket",
    notify_url="https://example.com/callback/alipay",
    return_url="https://example.com/thanks",
    )
    println("Checkout URL: \{url}")

    // Mobile website checkout works the same way.
    let wap_url = alipay.wap_pay_url(
    out_trade_no="hackathon-20261001-0003",
    total_fen=8800,
    subject="MoonBit Hackathon Ticket",
    notify_url="https://example.com/callback/alipay",
    )
    }

    #Alipay: async notification

    ///|
    async fn handle_alipay_notify(
    alipay : @alipay.Client,
    form : Array[(String, String)], // the urlencoded body of the POST
    ) -> String {
    // Verifies the RSA2 signature, then the app_id ownership.
    let notify = alipay.verify_notify(form)
    if notify.trade_status() is @alipay.TradeSuccess
    && notify.amount_matches(8800) {
    // Signed, from Alipay, and the amount matches — mark the order paid.
    }
    // Respond with the plain text "success" so Alipay stops retrying.
    @alipay.ack_success()
    }

    #Architecture

    your MoonBit application │ moon-pay-sdk ├── wechat Native / H5 / JSAPI payment, order query, callback verify & decrypt ├── miniprogram wx.login code exchange plus one-call mini-program checkout ├── alipay page / wap payment, order query, async notification verify ├── crypto RSA-SHA256 sign & verify, X.509 parsing, AES-256-GCM (mooncrypt) └── transport HTTPS over moonbitlang/async (epoll on Linux, kqueue on macOS) │ system dependencies ├── moonbitlang/async runtime └── OpenSSL (TLS transport, loaded at runtime by moonbitlang/async)

    Design notes:

    • Async runtime and transport — HTTP/HTTPS requests go through moonbitlang/async, whose TLS support is based on OpenSSL. The library is experimental; the SDK pins a known-good version.
    • Crypto via mooncrypt — request signing (RSA-SHA256), signature verification and callback decryption (AES-256-GCM) use the pure-MoonBit crypto stack moonbitstack/mooncrypt (algorithms), moonbitstack/mooncred (X.509 certificates) and moonbitstack/moonbase (base16/base64), all tested against specification vectors. No hand-rolled crypto and no OpenSSL C FFI of our own; the test suite additionally cross-checks signature outputs against openssl-generated reference signatures.
    • Native only — merchant private keys belong on servers, so the SDK targets the native backend and produces a self-contained binary.

    #Roadmap

    • Hackathon release — the feature set listed under Features
    • Next — refunds (WeChat v3, alipay.trade.refund), bill download, platform-certificate auto refresh, integration tests against provider sandboxes

    #License

    JsonError

    pub(all) suberror JsonError {
    Invalid(String)
    Missing(String)
    WrongType(String, String)
    }

    Why JSON could not be read the way the caller expected. Field access errors name the field; parse errors carry the underlying position detail.
    impl Show for JsonError

    JsonError::output

    fn JsonError::output(self : JsonError, logger : &Logger) -> Unit

    JsonError::to_string

    fn JsonError::to_string(self : JsonError) -> String

    SdkError

    pub(all) suberror SdkError {
    Api(ApiError)
    Network(String)
    Crypto(
    CryptoError
    )
    Config(String)
    }

    The one error type every public SDK function raises. Provider rejections keep their structured code; everything else names its layer.
    impl Show for SdkError

    SdkError::output

    fn SdkError::output(self : SdkError, logger : &Logger) -> Unit

    SdkError::to_string

    fn SdkError::to_string(self : SdkError) -> String

    ApiError

    pub(all) struct ApiError {
    code : String
    message : String
    http_status : Int?
    }

    A structured error reported by a payment provider: WeChat v3 answers with a JSON body carrying code and message, Alipay with an error code in the response form.

    ApiError::new

    fn ApiError::new(code~ : String, message~ : String, http_status? : Int?) -> ApiError

    array_field

    fn array_field(json : Json, key : String) -> Array[Json] raise JsonError

    The array value of an object field.

    bool_field

    fn bool_field(json : Json, key : String) -> Bool raise JsonError

    The boolean value of an object field.

    encode_json

    fn encode_json(json : Json) -> String

    Serialize a JSON value (compact form).

    format_yuan

    fn format_yuan(fen : Int) -> String raise SdkError

    Format an amount in fen as the two-decimal yuan string Alipay expects in total_amount: 8800 -> "88.00", 5 -> "0.05". The amount must be positive.

    int_field

    fn int_field(json : Json, key : String) -> Int raise JsonError

    The integer value of an object field. JSON numbers that carry a fraction are rejected rather than truncated.

    object_field

    fn object_field(json : Json, key : String) -> Json raise JsonError

    The nested object value of an object field, kept as JSON so field reads chain: int_field(object_field(json, "amount"), "total").

    parse_json

    fn parse_json(text : StringView) -> Json raise JsonError

    Parse JSON text, with the parse failure re-wrapped as JsonError::Invalid.

    parse_yuan_fen

    fn parse_yuan_fen(text : StringView) -> Int raise SdkError

    Parse the two-decimal yuan string Alipay returns in notifications and answers into integer fen: "88.00" -> 8800. Anything but exactly two decimals — one decimal, thousands separators, signs, blanks — is rejected, so a mismatch always means a malformed amount, never a silent rounding.

    string_field

    fn string_field(json : Json, key : String) -> String raise JsonError

    The string value of an object field.

    try_parse_json

    fn try_parse_json(text : StringView) -> Json?

    Parse JSON text, returning None instead of raising on malformed input.

    validate_alipay_out_trade_no

    fn validate_alipay_out_trade_no(out_trade_no : StringView) -> Unit raise SdkError

    Validate an out-trade number for Alipay: at most 64 characters, letters, digits, underscore and hyphen.

    validate_amount_fen

    fn validate_amount_fen(fen : Int) -> Unit raise SdkError

    Validate a WeChat order amount. Amounts travel as integer fen and must be positive — zero and negatives are rejected before they reach the gateway.

    validate_wechat_out_trade_no

    fn validate_wechat_out_trade_no(out_trade_no : StringView) -> Unit raise SdkError

    Validate an out-trade number for WeChat Pay: 6-32 characters, letters, digits and -_|*.