diff --git a/CHANGELOG.md b/CHANGELOG.md index ee9671e..422fb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Book](https://doc.rust-lang.org/cargo/reference/manifest.html#the-version-field) ## [Unreleased] +### Added +- A batteries-included payment gateway built around the core library. + ### Changed - Use webpki CA roots instead of native for better portability. diff --git a/Cargo.toml b/Cargo.toml index 3f97c03..9747351 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,74 +1,6 @@ -[package] -name = "acceptxmr" -version = "0.12.0" -edition = "2021" -rust-version = "1.65" -license = "MIT OR Apache-2.0" -description = "Accept monero in your application." -repository = "https://github.com/busyboredom/acceptxmr" -readme = "README.md" -keywords = ["crypto", "gateway", "monero", "payment", "xmr"] -categories = ["cryptography::cryptocurrencies"] +[workspace] -[lib] -name = "acceptxmr" -path = "src/lib.rs" - -[dependencies] -bincode = { version = "^2.0.0-rc.3", optional = true } -hex = "0.4" -http = "0.2" -hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } -hyper-rustls = { version = "0.24", features = ["logging", "http1", "http2", "tls12", "webpki-tokio"], default-features = false } -indexmap = "1" -log = "0.4" -md-5 = "0.10" -monero = "0.18" -rand = "0.8" -rand_chacha = "0.3" -serde = {version = "1", features = ["derive"], optional = true } -serde_json = "1" -sled = { version = "0.34", optional = true } -sqlite = { version = "0.30", optional = true } -strum = { version = "0.24", features = ["derive"] } -thiserror = "1" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } - -[features] -bincode = ["dep:bincode"] -in-memory = [] -serde = ["dep:serde"] -sled = ["bincode", "dep:sled"] -sqlite = ["bincode", "dep:sqlite"] - -[dev-dependencies] -actix = "0.13" -actix-files = "0.6" -actix-session = { version = "0.7", features = ["cookie-session"] } -actix-web = "4" -actix-web-actors = "4" -bytestring = "1" -env_logger = "0.10" -handlebars = { version = "4", features = ["dir_source"] } -httpmock = "0.6" -qrcode = "0.12" -serde = "1" -tempfile = "3" -test-case = "3" -# This is a workaround to enable features in tests. -acceptxmr = { path = ".", features = ["sled", "in-memory", "sqlite"] } - -[[example]] -name = "custom_storage" - -[[example]] -name = "nojs" -required-features = ["serde", "in-memory"] - -[[example]] -name = "persistence" -required-features = ["sqlite"] - -[[example]] -name = "websockets" -required-features = ["serde", "in-memory"] +members = [ + "server", + "library" +] diff --git a/README.md b/README.md index e3df1e4..63242b0 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,84 @@ [![BuildStatus](https://github.com/busyboredom/acceptxmr/workflows/CI/badge.svg)](https://img.shields.io/github/actions/workflow/status/busyboredom/acceptxmr/ci.yml?branch=main) -[![Crates.io](https://img.shields.io/crates/v/acceptxmr.svg)](https://crates.io/crates/acceptxmr) -[![Documentation](https://docs.rs/acceptxmr/badge.svg)](https://docs.rs/acceptxmr) -[![MSRV](https://img.shields.io/badge/MSRV-1.65.0-blue)](https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html) # `AcceptXMR`: Accept Monero in Your Application +`AcceptXMR` aims to provide a simple, reliable, and efficient means to track +monero payments. -This library aims to provide a simple, reliable, and efficient means to track monero payments. +To track payments, `AcceptXMR` generates subaddresses using your private view +key and primary address. It then watches for monero sent to that subaddress +using a monero daemon of your choosing, updating the UI in realtime and +optionally performing a configurable callback once payment is confirmed. -To track payments, the `PaymentGateway` generates subaddresses using your private view key and -primary address. It then watches for monero sent to that subaddress using a monero daemon of your -choosing, your private view key and your primary address. +For a batteries-included payment gateway, see +[`AcceptXMR-Server`](./server/). -Use this library at your own risk, it is young and unproven. +For a slim & performant library to use in your rust applications, see +[`AcceptXMR`](./library/). ## Key Advantages * View pair only, no hot wallet. * Subaddress based. -* Pending invoices can be stored persistently, enabling recovery from power loss. +* Pending invoices can be stored persistently, enabling recovery from power + loss. * Number of confirmations is configurable per-invoice. * Ignores transactions with timelocks. * Payment can occur over multiple transactions. ## Security +`AcceptXMR` is non-custodial, and does not require a hot wallet. However, it +does require your private view key and primary address for scanning outputs. If +keeping these private is important to you, please take appropriate precautions +to secure the platform you run your application on. -`AcceptXMR` is non-custodial, and does not require a hot wallet. However, it does require your -private view key and primary address for scanning outputs. If keeping these private is important -to you, please take appropriate precautions to secure the platform you run your application on. - -Also note that anonymity networks like TOR are not currently supported for RPC calls. This -means that your network traffic will reveal that you are interacting with the monero network. +Also note that anonymity networks like TOR are not currently supported for RPC +calls. This means that your network traffic will reveal that you are interacting +with the monero network. ## Reliability +`AcceptXMR` strives for reliability, but that attempt may not be successful. It +is young and unproven, and relies on several crates which are undergoing rapid +changes themselves. For example, one of the built-in storage layer +implementations ([`Sled`](https://docs.rs/sled)) is still in beta. -This library strives for reliability, but that attempt may not be successful. `AcceptXMR` is young -and unproven, and relies on several crates which are undergoing rapid changes themselves For -example, the primary storage layer implementation ([`Sled`](https://docs.rs/sled)) is still in beta. - -That said, this payment gateway should survive unexpected power loss thanks to the ability to flush -pending invoices to disk each time new blocks/transactions are scanned. A best effort is made to -keep the scanning thread free any of potential panics, and RPC calls in the scanning thread are -logged on failure and repeated next scan. In the event that an error does occur, the liberal use of -logging within this library will hopefully facilitate a speedy diagnosis an correction. +That said, `AcceptXMR` can survive unexpected power loss thanks to the ability +to flush pending invoices to disk each time new blocks/transactions are scanned. +A best effort is made to keep the scanning thread free any of potential panics, +and RPC calls in the scanning thread are logged on failure and repeated next +scan. In the event that an error does occur, the liberal use of logging within +`AcceptXMR` will hopefully facilitate a speedy diagnosis an correction. -Use this library at your own risk. +Use `AcceptXMR` at your own risk. ## Performance +It is recommended that you host your own monero daemon on the same local +network. Network and daemon slowness are the primary cause of high invoice +update latency in the majority of use cases. -It is strongly recommended that you host your own monero daemon on the same local network. Network -and daemon slowness are the primary cause of high invoice update latency in the majority of use -cases. - -To reduce the average latency before receiving invoice updates, you may also consider lowering -the `PaymentGateway`'s `scan_interval` below the default of 1 second: -```rust -use acceptxmr::PaymentGateway; -use std::time::Duration; - -let private_view_key = - "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; -let primary_address = - "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; - -let store = InMemory::new(); +To reduce the average latency before receiving invoice updates, you may also +consider lowering the gateway's scanning interval below the default of 1 second. +If using the `AcceptXMR` library, this can be done using the `scan_interval` +method of the `PaymentGatewayBuilder`. If using the standalone +`AcceptXMR-Server`, the scanning interval can be set in config. -let payment_gateway = PaymentGateway::builder( - private_view_key.to_string(), - primary_address.to_string(), - store -) -.scan_interval(Duration::from_millis(100)) // Scan for updates every 100 ms. -.build()?; -``` - -Please note that `scan_interval` is the minimum time between scanning for updates. If your -daemon's response time is already greater than your `scan_interval` or if your CPU is unable to -scan new transactions fast enough, reducing your `scan_interval` will do nothing. +Note that lowering the gateway's scanning interval will do nothing if latency to +your chosen node is slower than the scan interval. ## License - Licensed under either of - * Apache License, Version 2.0 - ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license - ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or + http://opensource.org/licenses/MIT) at your option. ## Contribution - Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. ### Donations - AcceptXMR is a hobby project which generates no revenue for the developer(s). Donations from generous users and community members help keep it economically viable to work on. diff --git a/library/Cargo.toml b/library/Cargo.toml new file mode 100644 index 0000000..3f97c03 --- /dev/null +++ b/library/Cargo.toml @@ -0,0 +1,74 @@ +[package] +name = "acceptxmr" +version = "0.12.0" +edition = "2021" +rust-version = "1.65" +license = "MIT OR Apache-2.0" +description = "Accept monero in your application." +repository = "https://github.com/busyboredom/acceptxmr" +readme = "README.md" +keywords = ["crypto", "gateway", "monero", "payment", "xmr"] +categories = ["cryptography::cryptocurrencies"] + +[lib] +name = "acceptxmr" +path = "src/lib.rs" + +[dependencies] +bincode = { version = "^2.0.0-rc.3", optional = true } +hex = "0.4" +http = "0.2" +hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } +hyper-rustls = { version = "0.24", features = ["logging", "http1", "http2", "tls12", "webpki-tokio"], default-features = false } +indexmap = "1" +log = "0.4" +md-5 = "0.10" +monero = "0.18" +rand = "0.8" +rand_chacha = "0.3" +serde = {version = "1", features = ["derive"], optional = true } +serde_json = "1" +sled = { version = "0.34", optional = true } +sqlite = { version = "0.30", optional = true } +strum = { version = "0.24", features = ["derive"] } +thiserror = "1" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } + +[features] +bincode = ["dep:bincode"] +in-memory = [] +serde = ["dep:serde"] +sled = ["bincode", "dep:sled"] +sqlite = ["bincode", "dep:sqlite"] + +[dev-dependencies] +actix = "0.13" +actix-files = "0.6" +actix-session = { version = "0.7", features = ["cookie-session"] } +actix-web = "4" +actix-web-actors = "4" +bytestring = "1" +env_logger = "0.10" +handlebars = { version = "4", features = ["dir_source"] } +httpmock = "0.6" +qrcode = "0.12" +serde = "1" +tempfile = "3" +test-case = "3" +# This is a workaround to enable features in tests. +acceptxmr = { path = ".", features = ["sled", "in-memory", "sqlite"] } + +[[example]] +name = "custom_storage" + +[[example]] +name = "nojs" +required-features = ["serde", "in-memory"] + +[[example]] +name = "persistence" +required-features = ["sqlite"] + +[[example]] +name = "websockets" +required-features = ["serde", "in-memory"] diff --git a/library/README.md b/library/README.md new file mode 100644 index 0000000..ba0a578 --- /dev/null +++ b/library/README.md @@ -0,0 +1,69 @@ +[![BuildStatus](https://github.com/busyboredom/acceptxmr/workflows/CI/badge.svg)](https://img.shields.io/github/actions/workflow/status/busyboredom/acceptxmr/ci.yml?branch=main) +[![Crates.io](https://img.shields.io/crates/v/acceptxmr.svg)](https://crates.io/crates/acceptxmr) +[![Documentation](https://docs.rs/acceptxmr/badge.svg)](https://docs.rs/acceptxmr) +[![MSRV](https://img.shields.io/badge/MSRV-1.65.0-blue)](https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html) + +# `AcceptXMR`: Accept Monero in Your Application +`AcceptXMR` is a library for building payment gateways. + +For a batteries-included gateway, please see +[`AcceptXMR-Server`](../server/). + +## Getting Started + +To use `AcceptXMR` in your rust project, first add it to your `Cargo.toml`. For +example if you intend to use the `Sqlite` storage backend and need `serde` +support, you should add this to your `Cargo.toml`: +```toml +[dependencies] +acceptxmr = { version = "0.12", features = ["serde", "sqlite"] } +``` +You can then create and run a `PaymentGateway`: +```rust +use acceptxmr::{PaymentGateway, storage::stores::Sqlite}; +use std::time::Duration; + +let private_view_key = + "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; +let primary_address = + "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; + +let store = Sqlite::new("AcceptXMR_DB", "invoices")?; + +let payment_gateway = PaymentGateway::builder( + private_view_key.to_string(), + primary_address.to_string(), + store +) +.daemon_url("https://node.example.com") // Specify a node. +.scan_interval(Duration::from_millis(500)) // Scan for updates every 500 ms. +.build()?; + +payment_gateway.run()?; +``` +Finally, you can create invoices and subscribe to them so you know when they get +paid: +```rust +// Oh hey, a customer is checking out! +let invoice_id = payment_gateway.new_invoice( + 100 * 10 ** 9, // We'll charge 100 millineros, + 0, // require 0 confirmations, + 10, // expire in 10 blocks, + "Large Cheese Pizza".to_string() // and get the order right. +)?; + +// We can now subscribe to updates to the pizza invoice. +let subscriber = payment_gateway.subscribe(invoice_id)? + .expect("invoice doesn't exist"); + +// Have we been paid yet? +let update = subscriber.recv().await.expect("channel closed"); + +if update.is_confirmed() { + // Great, ship the pizza and stop tracking the invoice. + println!("Invoice for \"{}\" paid", update.description()); + payment_gateway.remove_invoice(invoice_id)?; +} +``` +For more detailed documentation, see [docs.rs](https://docs.rs/acceptxmr) or the +[examples](./examples/). \ No newline at end of file diff --git a/examples/custom_storage/main.rs b/library/examples/custom_storage/main.rs similarity index 100% rename from examples/custom_storage/main.rs rename to library/examples/custom_storage/main.rs diff --git a/examples/nojs/main.rs b/library/examples/nojs/main.rs similarity index 96% rename from examples/nojs/main.rs rename to library/examples/nojs/main.rs index e0c2673..6139952 100644 --- a/examples/nojs/main.rs +++ b/library/examples/nojs/main.rs @@ -44,7 +44,7 @@ async fn main() -> std::io::Result<()> { primary_address.to_string(), InMemory::new(), ) - .daemon_url("http://node.sethforprivacy.com:18089".to_string()) + .daemon_url("http://xmr-node.cakewallet.com:18081".to_string()) .build() .expect("failed to build payment gateway"); info!("Payment gateway created."); @@ -87,7 +87,7 @@ async fn main() -> std::io::Result<()> { // Templating setup. let mut handlebars = Handlebars::new(); handlebars - .register_templates_directory(".html", "./examples/nojs/static/templates") + .register_templates_directory(".html", "./library/examples/nojs/static/templates") .expect("failed to register template directory"); handlebars.register_escape_fn(no_escape); @@ -107,7 +107,7 @@ async fn main() -> std::io::Result<()> { .app_data(handlebars_ref.clone()) .service(start_checkout) .service(checkout) - .service(Files::new("", "./examples/nojs/static").index_file("index.html")) + .service(Files::new("", "./library/examples/nojs/static").index_file("index.html")) }) .bind("0.0.0.0:8080")? .run() diff --git a/examples/nojs/static/acceptxmr.css b/library/examples/nojs/static/acceptxmr.css similarity index 100% rename from examples/nojs/static/acceptxmr.css rename to library/examples/nojs/static/acceptxmr.css diff --git a/examples/nojs/static/expired.html b/library/examples/nojs/static/expired.html similarity index 100% rename from examples/nojs/static/expired.html rename to library/examples/nojs/static/expired.html diff --git a/examples/nojs/static/favicon.ico b/library/examples/nojs/static/favicon.ico similarity index 100% rename from examples/nojs/static/favicon.ico rename to library/examples/nojs/static/favicon.ico diff --git a/examples/nojs/static/index.html b/library/examples/nojs/static/index.html similarity index 100% rename from examples/nojs/static/index.html rename to library/examples/nojs/static/index.html diff --git a/examples/nojs/static/refresh.png b/library/examples/nojs/static/refresh.png similarity index 100% rename from examples/nojs/static/refresh.png rename to library/examples/nojs/static/refresh.png diff --git a/examples/nojs/static/templates/checkout.html b/library/examples/nojs/static/templates/checkout.html similarity index 100% rename from examples/nojs/static/templates/checkout.html rename to library/examples/nojs/static/templates/checkout.html diff --git a/examples/persistence/main.rs b/library/examples/persistence/main.rs similarity index 92% rename from examples/persistence/main.rs rename to library/examples/persistence/main.rs index 6d0b016..3a1dbe2 100644 --- a/examples/persistence/main.rs +++ b/library/examples/persistence/main.rs @@ -18,7 +18,7 @@ async fn main() { let primary_address = "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; // Use an Sqlite database for persistent storage of pending invoices. - let store = Sqlite::new("examples/persistence/AcceptXMR_DB", "invoices").unwrap(); + let store = Sqlite::new("library/examples/persistence/AcceptXMR_DB", "invoices").unwrap(); let payment_gateway = PaymentGatewayBuilder::new( private_view_key.to_string(), @@ -50,7 +50,7 @@ async fn main() { power_failure(payment_gateway); // Reconstruct the gateway... - let store = Sqlite::new("examples/persistence/AcceptXMR_DB", "invoices").unwrap(); + let store = Sqlite::new("library/examples/persistence/AcceptXMR_DB", "invoices").unwrap(); let payment_gateway = PaymentGatewayBuilder::new( private_view_key.to_string(), primary_address.to_string(), diff --git a/examples/websockets/main.rs b/library/examples/websockets/main.rs similarity index 98% rename from examples/websockets/main.rs rename to library/examples/websockets/main.rs index a333dfa..a3d6d35 100644 --- a/examples/websockets/main.rs +++ b/library/examples/websockets/main.rs @@ -57,7 +57,7 @@ async fn main() -> std::io::Result<()> { primary_address.to_string(), invoice_store, ) - .daemon_url("http://node.sethforprivacy.com:18089".to_string()) + .daemon_url("http://xmr-node.cakewallet.com:18081".to_string()) .build() .expect("failed to build payment gateway"); info!("Payment gateway created."); @@ -111,7 +111,9 @@ async fn main() -> std::io::Result<()> { .service(update) .service(checkout) .service(websocket) - .service(Files::new("", "./examples/websockets/static").index_file("index.html")) + .service( + Files::new("", "./library/examples/websockets/static").index_file("index.html"), + ) }) .bind("0.0.0.0:8080")? .run() diff --git a/examples/websockets/static/acceptxmr.css b/library/examples/websockets/static/acceptxmr.css similarity index 100% rename from examples/websockets/static/acceptxmr.css rename to library/examples/websockets/static/acceptxmr.css diff --git a/examples/websockets/static/acceptxmr.js b/library/examples/websockets/static/acceptxmr.js similarity index 100% rename from examples/websockets/static/acceptxmr.js rename to library/examples/websockets/static/acceptxmr.js diff --git a/examples/websockets/static/favicon.ico b/library/examples/websockets/static/favicon.ico similarity index 100% rename from examples/websockets/static/favicon.ico rename to library/examples/websockets/static/favicon.ico diff --git a/examples/websockets/static/index.html b/library/examples/websockets/static/index.html similarity index 100% rename from examples/websockets/static/index.html rename to library/examples/websockets/static/index.html diff --git a/examples/websockets/static/vendor/qrcode.js b/library/examples/websockets/static/vendor/qrcode.js similarity index 100% rename from examples/websockets/static/vendor/qrcode.js rename to library/examples/websockets/static/vendor/qrcode.js diff --git a/src/caching/block_cache.rs b/library/src/caching/block_cache.rs similarity index 100% rename from src/caching/block_cache.rs rename to library/src/caching/block_cache.rs diff --git a/src/caching/mod.rs b/library/src/caching/mod.rs similarity index 100% rename from src/caching/mod.rs rename to library/src/caching/mod.rs diff --git a/src/caching/subaddress_cache.rs b/library/src/caching/subaddress_cache.rs similarity index 100% rename from src/caching/subaddress_cache.rs rename to library/src/caching/subaddress_cache.rs diff --git a/src/caching/txpool_cache.rs b/library/src/caching/txpool_cache.rs similarity index 100% rename from src/caching/txpool_cache.rs rename to library/src/caching/txpool_cache.rs diff --git a/src/invoice.rs b/library/src/invoice.rs similarity index 100% rename from src/invoice.rs rename to library/src/invoice.rs diff --git a/src/lib.rs b/library/src/lib.rs similarity index 100% rename from src/lib.rs rename to library/src/lib.rs diff --git a/src/payment_gateway.rs b/library/src/payment_gateway.rs similarity index 100% rename from src/payment_gateway.rs rename to library/src/payment_gateway.rs diff --git a/src/pubsub.rs b/library/src/pubsub.rs similarity index 100% rename from src/pubsub.rs rename to library/src/pubsub.rs diff --git a/src/rpc/authentication.rs b/library/src/rpc/authentication.rs similarity index 100% rename from src/rpc/authentication.rs rename to library/src/rpc/authentication.rs diff --git a/src/rpc/mod.rs b/library/src/rpc/mod.rs similarity index 100% rename from src/rpc/mod.rs rename to library/src/rpc/mod.rs diff --git a/src/scanner.rs b/library/src/scanner.rs similarity index 100% rename from src/scanner.rs rename to library/src/scanner.rs diff --git a/library/src/static/acceptxmr.css b/library/src/static/acceptxmr.css new file mode 100644 index 0000000..0880322 --- /dev/null +++ b/library/src/static/acceptxmr.css @@ -0,0 +1,151 @@ +body { + background-color: #4d4d4d; +} + +.acceptxmr { + max-width: 80ch; + margin: auto; + background-color: #f26822; + border-radius: 1em; + display: flex; + flex-flow: column wrap; + align-items: center; +} + +.acceptxmr button { + background-color: #0088ff; + border-radius: 0.5em; + border: 0; + margin: 0; + font-size: medium; +} + +.acceptxmr button:hover { + background-color: #005fb3; + cursor: pointer; +} + +.acceptxmr button:disabled { + background-color: #979797; +} + +.acceptxmr #message-container { + margin: 1em; + flex: 2 1 20em; +} + +.acceptxmr #payment-content { + display: none; +} + +.acceptxmr .content button { + margin: 1em; + flex: 1 2 10ch; + padding: 12px 0.5em; + align-self: flex-end; +} + +.acceptxmr textarea { + border-radius: 0.5em; + border: 0; + padding: 0.5em; + width: 100%; + margin: 0; + background-color: #4d4d4d; + color: #ffffff; + resize: vertical; +} + +.acceptxmr .instruction-container { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; +} + +.acceptxmr .instruction-container p { + margin: 0.1em; + font-size: large; +} + +.acceptxmr .instruction-container button { + margin: 0.5em; + padding: 0.25em 0.5em; +} + +.acceptxmr .warning { + animation: acceptxmr-fade 2s linear; + animation-iteration-count: infinite; +} + +@keyframes acceptxmr-fade { + 0%,100% { opacity: 0.2 } + 50% { opacity: 1 } +} + +.acceptxmr hr { + width: 90%; + border-style: solid; + border-color: #000000; + margin: 0; +} + +.content { + width: 100%; + display: flex; + flex-flow: row wrap; +} + +.acceptxmr .qrcode-container { + flex: 1 1 10ch; + margin: 1em; + display: flex; + justify-content: center; +} + +.acceptxmr .qrcode-container svg { + border-radius: 1em; + min-width: 18ch; + align-self: center; + background-color: #dddddd; +} + +.acceptxmr .details { + flex: 2 2 30ch; + margin: 0 1em 1em 1em; + overflow: hidden; + align-self: center; +} + +.acceptxmr #address-container { + display: flex; + height: 3em; + border-radius: 0.5em; + margin-bottom: 0.5em; + background-color: #ffffff; + justify-content: space-between; +} + +.acceptxmr #address-container p { + overflow-x: scroll; + overflow-y: hidden; + margin: 0 0.5em 0 0.5em; + height: 100%; + width: 100%; + display: flex; + align-items: center; +} + +.acceptxmr #address-container button { + border-radius: 0 0.5em 0.5em 0; + height: 100%; + padding: 0 1em; + margin: 0; +} + +.acceptxmr .status { + margin: 0 0 0 0; + border-radius: 0.5em; + padding: 0.5em; + background-color: #ffffff; +} diff --git a/library/src/static/acceptxmr.js b/library/src/static/acceptxmr.js new file mode 100644 index 0000000..ff24d34 --- /dev/null +++ b/library/src/static/acceptxmr.js @@ -0,0 +1,152 @@ +// Try to load existing invoice on page load. +async function init() { + let response = await fetch("/update"); + if (response.status !== 410 ) { + let invoiceUpdate = await response.json(); + displayInvoiceUpdate(invoiceUpdate); + await next(true); + } +} +init() + +async function next(hasAddress) { + // Hide prep stuff, show payment stuff. + document.getElementById("preperation-content").style.display = "None"; + document.getElementById("payment-content").style.display = "inherit"; + + // Create invoice. + if (!hasAddress) { + document.getElementById("instruction").innerHTML = "Loading..."; + await newAddress(); + } else { + newWebsocket(); + } +} + +async function newAddress() { + const message = document.getElementById("message").value; + const checkOutInfo = { + method: "POST", + body: JSON.stringify({ + "message": message + }), + headers: { + 'content-type': 'application/json' + } + }; + + await fetch("/checkout", checkOutInfo); + newWebsocket(); + + let response = await fetch("/update"); + let invoiceUpdate = await response.json(); + displayInvoiceUpdate(invoiceUpdate); +} + +function displayInvoiceUpdate(invoiceUpdate) { + console.log(invoiceUpdate); + + // Show paid/due. + document.getElementById("paid").innerHTML = picoToXMR(invoiceUpdate.amount_paid); + document.getElementById("due").innerHTML = picoToXMR(invoiceUpdate.amount_requested); + + // Show confirmations/required. + var confirmations = invoiceUpdate.confirmations; + document.getElementById("confirmations").innerHTML = Math.max(0, confirmations); + document.getElementById("confirmations-required").innerHTML = invoiceUpdate.confirmations_required; + + // Show instructive text depending on invoice state. + var instructionString = "Loading..."; + var instructionClass = "acceptxmr-instruction"; + var newAddressBtnHidden = true; + var closeReason = null; + if (confirmations !== null && confirmations >= invoiceUpdate.confirmations_required) { + instructionString = "Paid! Thank you" + closeReason = "Confirmed"; + } else if (invoiceUpdate.amount_paid >= invoiceUpdate.amount_requested) { + instructionString = "Paid! Waiting for Confirmation..." + } else if (invoiceUpdate.expiration_in > 2) { + instructionString = "Send Monero to Address Below" + } else if (invoiceUpdate.expiration_in > 0) { + instructionString = "Address Expiring Soon"; + instructionClass += " warning"; + newAddressBtnHidden = false; + } else { + instructionString = "Address Expired!"; + newAddressBtnHidden = false; + closeReason = "Expired"; + } + document.getElementById("instruction").innerHTML = instructionString; + document.getElementById("instruction").classList = instructionClass; + + // Hide address if nearing expiration. + document.getElementById("new-address-btn").hidden = newAddressBtnHidden; + document.getElementById("address-copy-btn").disabled = !newAddressBtnHidden; + if (newAddressBtnHidden) { + document.getElementById("address").innerHTML = invoiceUpdate.address; + + const qr = qrcode(0, "M"); + qr.addData(invoiceUpdate.uri); + qr.make(); + document.getElementById('qrcode-container').innerHTML = qr.createSvgTag({ scalable: true }); + } else { + document.getElementById("address").innerHTML = "Expiring or expired..."; + document.getElementById('qrcode-container').innerHTML = ""; + } + + return closeReason; +} + +function newWebsocket() { + // Close websocket if it already exists. + if (typeof window.acceptxmrSocket != 'undefined') { + window.acceptxmrSocket.close(1000, "New Address"); + } + + // Open websocket. + window.acceptxmrSocket = new WebSocket("ws://localhost:8080/ws/"); + + window.acceptxmrSocket.onmessage = function (event) { + let closeReason = displayInvoiceUpdate(JSON.parse(event.data)); + if (closeReason != null) { + window.acceptxmrSocket.close(1000, closeReason); + } + } + + // If the websocket closes cleanly, log it. Otherwise, alert the user. + window.acceptxmrSocket.onclose = function (event) { + if (event.code === 1000) { + console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); + } else { + // Server process killed or network down. + // Event.code is usually 1006 in this case. + alert('Connection died. If you have paid already, rest assured that it will still be processed.'); + } + }; + + window.acceptxmrSocket.onerror = function (error) { + alert(`[error] ${error.message}`); + }; +} + +// Convert from piconeros to monero. +function picoToXMR(amount) { + const divisor = 1_000_000_000_000; + const xmr = Math.floor(amount / divisor) + amount % divisor / divisor; + return new Intl.NumberFormat(undefined, { maximumSignificantDigits: 20 }).format(xmr); +} + +// Make the copy button work. +function copyInvoiceAddress() { + // Get the text field + const copyText = document.getElementById("address"); + + // Copy the text inside the text field + navigator.clipboard.writeText(copyText.innerHTML); + + // Provide feedback + document.getElementById("address-copy-btn").innerHTML = "Copied!"; + setTimeout(function () { + document.getElementById("address-copy-btn").innerHTML = "Copy"; + }, 1000); +} diff --git a/library/src/static/favicon.ico b/library/src/static/favicon.ico new file mode 100644 index 0000000..37a6dae Binary files /dev/null and b/library/src/static/favicon.ico differ diff --git a/library/src/static/index.html b/library/src/static/index.html new file mode 100644 index 0000000..b9511dd --- /dev/null +++ b/library/src/static/index.html @@ -0,0 +1,40 @@ + + +AcceptXMR Websocket Demo + +
+
+

AcceptXMR Websocket Demo

+ +
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+

+ +
+ +

+ Paid: / 0.00000000 XMR
+ Confirmations: + 0 / 0
+

+
+
+
+ + + diff --git a/library/src/static/vendor/qrcode.js b/library/src/static/vendor/qrcode.js new file mode 100644 index 0000000..fde008b --- /dev/null +++ b/library/src/static/vendor/qrcode.js @@ -0,0 +1,2297 @@ +//--------------------------------------------------------------------- +// +// QR Code Generator for JavaScript +// +// Copyright (c) 2009 Kazuhiko Arase +// +// URL: http://www.d-project.com/ +// +// Licensed under the MIT license: +// http://www.opensource.org/licenses/mit-license.php +// +// The word 'QR Code' is registered trademark of +// DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/faqpatent-e.html +// +//--------------------------------------------------------------------- + +var qrcode = function() { + + //--------------------------------------------------------------------- + // qrcode + //--------------------------------------------------------------------- + + /** + * qrcode + * @param typeNumber 1 to 40 + * @param errorCorrectionLevel 'L','M','Q','H' + */ + var qrcode = function(typeNumber, errorCorrectionLevel) { + + var PAD0 = 0xEC; + var PAD1 = 0x11; + + var _typeNumber = typeNumber; + var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel]; + var _modules = null; + var _moduleCount = 0; + var _dataCache = null; + var _dataList = []; + + var _this = {}; + + var makeImpl = function(test, maskPattern) { + + _moduleCount = _typeNumber * 4 + 17; + _modules = function(moduleCount) { + var modules = new Array(moduleCount); + for (var row = 0; row < moduleCount; row += 1) { + modules[row] = new Array(moduleCount); + for (var col = 0; col < moduleCount; col += 1) { + modules[row][col] = null; + } + } + return modules; + }(_moduleCount); + + setupPositionProbePattern(0, 0); + setupPositionProbePattern(_moduleCount - 7, 0); + setupPositionProbePattern(0, _moduleCount - 7); + setupPositionAdjustPattern(); + setupTimingPattern(); + setupTypeInfo(test, maskPattern); + + if (_typeNumber >= 7) { + setupTypeNumber(test); + } + + if (_dataCache == null) { + _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList); + } + + mapData(_dataCache, maskPattern); + }; + + var setupPositionProbePattern = function(row, col) { + + for (var r = -1; r <= 7; r += 1) { + + if (row + r <= -1 || _moduleCount <= row + r) continue; + + for (var c = -1; c <= 7; c += 1) { + + if (col + c <= -1 || _moduleCount <= col + c) continue; + + if ( (0 <= r && r <= 6 && (c == 0 || c == 6) ) + || (0 <= c && c <= 6 && (r == 0 || r == 6) ) + || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) { + _modules[row + r][col + c] = true; + } else { + _modules[row + r][col + c] = false; + } + } + } + }; + + var getBestMaskPattern = function() { + + var minLostPoint = 0; + var pattern = 0; + + for (var i = 0; i < 8; i += 1) { + + makeImpl(true, i); + + var lostPoint = QRUtil.getLostPoint(_this); + + if (i == 0 || minLostPoint > lostPoint) { + minLostPoint = lostPoint; + pattern = i; + } + } + + return pattern; + }; + + var setupTimingPattern = function() { + + for (var r = 8; r < _moduleCount - 8; r += 1) { + if (_modules[r][6] != null) { + continue; + } + _modules[r][6] = (r % 2 == 0); + } + + for (var c = 8; c < _moduleCount - 8; c += 1) { + if (_modules[6][c] != null) { + continue; + } + _modules[6][c] = (c % 2 == 0); + } + }; + + var setupPositionAdjustPattern = function() { + + var pos = QRUtil.getPatternPosition(_typeNumber); + + for (var i = 0; i < pos.length; i += 1) { + + for (var j = 0; j < pos.length; j += 1) { + + var row = pos[i]; + var col = pos[j]; + + if (_modules[row][col] != null) { + continue; + } + + for (var r = -2; r <= 2; r += 1) { + + for (var c = -2; c <= 2; c += 1) { + + if (r == -2 || r == 2 || c == -2 || c == 2 + || (r == 0 && c == 0) ) { + _modules[row + r][col + c] = true; + } else { + _modules[row + r][col + c] = false; + } + } + } + } + } + }; + + var setupTypeNumber = function(test) { + + var bits = QRUtil.getBCHTypeNumber(_typeNumber); + + for (var i = 0; i < 18; i += 1) { + var mod = (!test && ( (bits >> i) & 1) == 1); + _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod; + } + + for (var i = 0; i < 18; i += 1) { + var mod = (!test && ( (bits >> i) & 1) == 1); + _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod; + } + }; + + var setupTypeInfo = function(test, maskPattern) { + + var data = (_errorCorrectionLevel << 3) | maskPattern; + var bits = QRUtil.getBCHTypeInfo(data); + + // vertical + for (var i = 0; i < 15; i += 1) { + + var mod = (!test && ( (bits >> i) & 1) == 1); + + if (i < 6) { + _modules[i][8] = mod; + } else if (i < 8) { + _modules[i + 1][8] = mod; + } else { + _modules[_moduleCount - 15 + i][8] = mod; + } + } + + // horizontal + for (var i = 0; i < 15; i += 1) { + + var mod = (!test && ( (bits >> i) & 1) == 1); + + if (i < 8) { + _modules[8][_moduleCount - i - 1] = mod; + } else if (i < 9) { + _modules[8][15 - i - 1 + 1] = mod; + } else { + _modules[8][15 - i - 1] = mod; + } + } + + // fixed module + _modules[_moduleCount - 8][8] = (!test); + }; + + var mapData = function(data, maskPattern) { + + var inc = -1; + var row = _moduleCount - 1; + var bitIndex = 7; + var byteIndex = 0; + var maskFunc = QRUtil.getMaskFunction(maskPattern); + + for (var col = _moduleCount - 1; col > 0; col -= 2) { + + if (col == 6) col -= 1; + + while (true) { + + for (var c = 0; c < 2; c += 1) { + + if (_modules[row][col - c] == null) { + + var dark = false; + + if (byteIndex < data.length) { + dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1); + } + + var mask = maskFunc(row, col - c); + + if (mask) { + dark = !dark; + } + + _modules[row][col - c] = dark; + bitIndex -= 1; + + if (bitIndex == -1) { + byteIndex += 1; + bitIndex = 7; + } + } + } + + row += inc; + + if (row < 0 || _moduleCount <= row) { + row -= inc; + inc = -inc; + break; + } + } + } + }; + + var createBytes = function(buffer, rsBlocks) { + + var offset = 0; + + var maxDcCount = 0; + var maxEcCount = 0; + + var dcdata = new Array(rsBlocks.length); + var ecdata = new Array(rsBlocks.length); + + for (var r = 0; r < rsBlocks.length; r += 1) { + + var dcCount = rsBlocks[r].dataCount; + var ecCount = rsBlocks[r].totalCount - dcCount; + + maxDcCount = Math.max(maxDcCount, dcCount); + maxEcCount = Math.max(maxEcCount, ecCount); + + dcdata[r] = new Array(dcCount); + + for (var i = 0; i < dcdata[r].length; i += 1) { + dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset]; + } + offset += dcCount; + + var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); + var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1); + + var modPoly = rawPoly.mod(rsPoly); + ecdata[r] = new Array(rsPoly.getLength() - 1); + for (var i = 0; i < ecdata[r].length; i += 1) { + var modIndex = i + modPoly.getLength() - ecdata[r].length; + ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0; + } + } + + var totalCodeCount = 0; + for (var i = 0; i < rsBlocks.length; i += 1) { + totalCodeCount += rsBlocks[i].totalCount; + } + + var data = new Array(totalCodeCount); + var index = 0; + + for (var i = 0; i < maxDcCount; i += 1) { + for (var r = 0; r < rsBlocks.length; r += 1) { + if (i < dcdata[r].length) { + data[index] = dcdata[r][i]; + index += 1; + } + } + } + + for (var i = 0; i < maxEcCount; i += 1) { + for (var r = 0; r < rsBlocks.length; r += 1) { + if (i < ecdata[r].length) { + data[index] = ecdata[r][i]; + index += 1; + } + } + } + + return data; + }; + + var createData = function(typeNumber, errorCorrectionLevel, dataList) { + + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel); + + var buffer = qrBitBuffer(); + + for (var i = 0; i < dataList.length; i += 1) { + var data = dataList[i]; + buffer.put(data.getMode(), 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) ); + data.write(buffer); + } + + // calc num max data. + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i += 1) { + totalDataCount += rsBlocks[i].dataCount; + } + + if (buffer.getLengthInBits() > totalDataCount * 8) { + throw 'code length overflow. (' + + buffer.getLengthInBits() + + '>' + + totalDataCount * 8 + + ')'; + } + + // end code + if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { + buffer.put(0, 4); + } + + // padding + while (buffer.getLengthInBits() % 8 != 0) { + buffer.putBit(false); + } + + // padding + while (true) { + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(PAD0, 8); + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(PAD1, 8); + } + + return createBytes(buffer, rsBlocks); + }; + + _this.addData = function(data, mode) { + + mode = mode || 'Byte'; + + var newData = null; + + switch(mode) { + case 'Numeric' : + newData = qrNumber(data); + break; + case 'Alphanumeric' : + newData = qrAlphaNum(data); + break; + case 'Byte' : + newData = qr8BitByte(data); + break; + case 'Kanji' : + newData = qrKanji(data); + break; + default : + throw 'mode:' + mode; + } + + _dataList.push(newData); + _dataCache = null; + }; + + _this.isDark = function(row, col) { + if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) { + throw row + ',' + col; + } + return _modules[row][col]; + }; + + _this.getModuleCount = function() { + return _moduleCount; + }; + + _this.make = function() { + if (_typeNumber < 1) { + var typeNumber = 1; + + for (; typeNumber < 40; typeNumber++) { + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, _errorCorrectionLevel); + var buffer = qrBitBuffer(); + + for (var i = 0; i < _dataList.length; i++) { + var data = _dataList[i]; + buffer.put(data.getMode(), 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) ); + data.write(buffer); + } + + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount; + } + + if (buffer.getLengthInBits() <= totalDataCount * 8) { + break; + } + } + + _typeNumber = typeNumber; + } + + makeImpl(false, getBestMaskPattern() ); + }; + + _this.createTableTag = function(cellSize, margin) { + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + var qrHtml = ''; + + qrHtml += ''; + qrHtml += ''; + + for (var r = 0; r < _this.getModuleCount(); r += 1) { + + qrHtml += ''; + + for (var c = 0; c < _this.getModuleCount(); c += 1) { + qrHtml += ''; + } + + qrHtml += ''; + qrHtml += '
'; + } + + qrHtml += '
'; + + return qrHtml; + }; + + _this.createSvgTag = function(cellSize, margin, alt, title) { + + var opts = {}; + if (typeof arguments[0] == 'object') { + // Called by options. + opts = arguments[0]; + // overwrite cellSize and margin. + cellSize = opts.cellSize; + margin = opts.margin; + alt = opts.alt; + title = opts.title; + } + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + // Compose alt property surrogate + alt = (typeof alt === 'string') ? {text: alt} : alt || {}; + alt.text = alt.text || null; + alt.id = (alt.text) ? alt.id || 'qrcode-description' : null; + + // Compose title property surrogate + title = (typeof title === 'string') ? {text: title} : title || {}; + title.text = title.text || null; + title.id = (title.text) ? title.id || 'qrcode-title' : null; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var c, mc, r, mr, qrSvg='', rect; + + rect = 'l' + cellSize + ',0 0,' + cellSize + + ' -' + cellSize + ',0 0,-' + cellSize + 'z '; + + qrSvg += '' + + escapeXml(title.text) + '' : ''; + qrSvg += (alt.text) ? '' + + escapeXml(alt.text) + '' : ''; + qrSvg += ''; + qrSvg += ''; + qrSvg += ''; + + return qrSvg; + }; + + _this.createDataURL = function(cellSize, margin) { + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var min = margin; + var max = size - margin; + + return createDataURL(size, size, function(x, y) { + if (min <= x && x < max && min <= y && y < max) { + var c = Math.floor( (x - min) / cellSize); + var r = Math.floor( (y - min) / cellSize); + return _this.isDark(r, c)? 0 : 1; + } else { + return 1; + } + } ); + }; + + _this.createImgTag = function(cellSize, margin, alt) { + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + + var img = ''; + img += '': escaped += '>'; break; + case '&': escaped += '&'; break; + case '"': escaped += '"'; break; + default : escaped += c; break; + } + } + return escaped; + }; + + var _createHalfASCII = function(margin) { + var cellSize = 1; + margin = (typeof margin == 'undefined')? cellSize * 2 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var min = margin; + var max = size - margin; + + var y, x, r1, r2, p; + + var blocks = { + '██': '█', + '█ ': '▀', + ' █': '▄', + ' ': ' ' + }; + + var blocksLastLineNoMargin = { + '██': '▀', + '█ ': '▀', + ' █': ' ', + ' ': ' ' + }; + + var ascii = ''; + for (y = 0; y < size; y += 2) { + r1 = Math.floor((y - min) / cellSize); + r2 = Math.floor((y + 1 - min) / cellSize); + for (x = 0; x < size; x += 1) { + p = '█'; + + if (min <= x && x < max && min <= y && y < max && _this.isDark(r1, Math.floor((x - min) / cellSize))) { + p = ' '; + } + + if (min <= x && x < max && min <= y+1 && y+1 < max && _this.isDark(r2, Math.floor((x - min) / cellSize))) { + p += ' '; + } + else { + p += '█'; + } + + // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square. + ascii += (margin < 1 && y+1 >= max) ? blocksLastLineNoMargin[p] : blocks[p]; + } + + ascii += '\n'; + } + + if (size % 2 && margin > 0) { + return ascii.substring(0, ascii.length - size - 1) + Array(size+1).join('▀'); + } + + return ascii.substring(0, ascii.length-1); + }; + + _this.createASCII = function(cellSize, margin) { + cellSize = cellSize || 1; + + if (cellSize < 2) { + return _createHalfASCII(margin); + } + + cellSize -= 1; + margin = (typeof margin == 'undefined')? cellSize * 2 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var min = margin; + var max = size - margin; + + var y, x, r, p; + + var white = Array(cellSize+1).join('██'); + var black = Array(cellSize+1).join(' '); + + var ascii = ''; + var line = ''; + for (y = 0; y < size; y += 1) { + r = Math.floor( (y - min) / cellSize); + line = ''; + for (x = 0; x < size; x += 1) { + p = 1; + + if (min <= x && x < max && min <= y && y < max && _this.isDark(r, Math.floor((x - min) / cellSize))) { + p = 0; + } + + // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square. + line += p ? white : black; + } + + for (r = 0; r < cellSize; r += 1) { + ascii += line + '\n'; + } + } + + return ascii.substring(0, ascii.length-1); + }; + + _this.renderTo2dContext = function(context, cellSize) { + cellSize = cellSize || 2; + var length = _this.getModuleCount(); + for (var row = 0; row < length; row++) { + for (var col = 0; col < length; col++) { + context.fillStyle = _this.isDark(row, col) ? 'black' : 'white'; + context.fillRect(row * cellSize, col * cellSize, cellSize, cellSize); + } + } + } + + return _this; + }; + + //--------------------------------------------------------------------- + // qrcode.stringToBytes + //--------------------------------------------------------------------- + + qrcode.stringToBytesFuncs = { + 'default' : function(s) { + var bytes = []; + for (var i = 0; i < s.length; i += 1) { + var c = s.charCodeAt(i); + bytes.push(c & 0xff); + } + return bytes; + } + }; + + qrcode.stringToBytes = qrcode.stringToBytesFuncs['default']; + + //--------------------------------------------------------------------- + // qrcode.createStringToBytes + //--------------------------------------------------------------------- + + /** + * @param unicodeData base64 string of byte array. + * [16bit Unicode],[16bit Bytes], ... + * @param numChars + */ + qrcode.createStringToBytes = function(unicodeData, numChars) { + + // create conversion map. + + var unicodeMap = function() { + + var bin = base64DecodeInputStream(unicodeData); + var read = function() { + var b = bin.read(); + if (b == -1) throw 'eof'; + return b; + }; + + var count = 0; + var unicodeMap = {}; + while (true) { + var b0 = bin.read(); + if (b0 == -1) break; + var b1 = read(); + var b2 = read(); + var b3 = read(); + var k = String.fromCharCode( (b0 << 8) | b1); + var v = (b2 << 8) | b3; + unicodeMap[k] = v; + count += 1; + } + if (count != numChars) { + throw count + ' != ' + numChars; + } + + return unicodeMap; + }(); + + var unknownChar = '?'.charCodeAt(0); + + return function(s) { + var bytes = []; + for (var i = 0; i < s.length; i += 1) { + var c = s.charCodeAt(i); + if (c < 128) { + bytes.push(c); + } else { + var b = unicodeMap[s.charAt(i)]; + if (typeof b == 'number') { + if ( (b & 0xff) == b) { + // 1byte + bytes.push(b); + } else { + // 2bytes + bytes.push(b >>> 8); + bytes.push(b & 0xff); + } + } else { + bytes.push(unknownChar); + } + } + } + return bytes; + }; + }; + + //--------------------------------------------------------------------- + // QRMode + //--------------------------------------------------------------------- + + var QRMode = { + MODE_NUMBER : 1 << 0, + MODE_ALPHA_NUM : 1 << 1, + MODE_8BIT_BYTE : 1 << 2, + MODE_KANJI : 1 << 3 + }; + + //--------------------------------------------------------------------- + // QRErrorCorrectionLevel + //--------------------------------------------------------------------- + + var QRErrorCorrectionLevel = { + L : 1, + M : 0, + Q : 3, + H : 2 + }; + + //--------------------------------------------------------------------- + // QRMaskPattern + //--------------------------------------------------------------------- + + var QRMaskPattern = { + PATTERN000 : 0, + PATTERN001 : 1, + PATTERN010 : 2, + PATTERN011 : 3, + PATTERN100 : 4, + PATTERN101 : 5, + PATTERN110 : 6, + PATTERN111 : 7 + }; + + //--------------------------------------------------------------------- + // QRUtil + //--------------------------------------------------------------------- + + var QRUtil = function() { + + var PATTERN_POSITION_TABLE = [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ]; + var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0); + var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0); + var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1); + + var _this = {}; + + var getBCHDigit = function(data) { + var digit = 0; + while (data != 0) { + digit += 1; + data >>>= 1; + } + return digit; + }; + + _this.getBCHTypeInfo = function(data) { + var d = data << 10; + while (getBCHDigit(d) - getBCHDigit(G15) >= 0) { + d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) ); + } + return ( (data << 10) | d) ^ G15_MASK; + }; + + _this.getBCHTypeNumber = function(data) { + var d = data << 12; + while (getBCHDigit(d) - getBCHDigit(G18) >= 0) { + d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) ); + } + return (data << 12) | d; + }; + + _this.getPatternPosition = function(typeNumber) { + return PATTERN_POSITION_TABLE[typeNumber - 1]; + }; + + _this.getMaskFunction = function(maskPattern) { + + switch (maskPattern) { + + case QRMaskPattern.PATTERN000 : + return function(i, j) { return (i + j) % 2 == 0; }; + case QRMaskPattern.PATTERN001 : + return function(i, j) { return i % 2 == 0; }; + case QRMaskPattern.PATTERN010 : + return function(i, j) { return j % 3 == 0; }; + case QRMaskPattern.PATTERN011 : + return function(i, j) { return (i + j) % 3 == 0; }; + case QRMaskPattern.PATTERN100 : + return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; }; + case QRMaskPattern.PATTERN101 : + return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; }; + case QRMaskPattern.PATTERN110 : + return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; }; + case QRMaskPattern.PATTERN111 : + return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; }; + + default : + throw 'bad maskPattern:' + maskPattern; + } + }; + + _this.getErrorCorrectPolynomial = function(errorCorrectLength) { + var a = qrPolynomial([1], 0); + for (var i = 0; i < errorCorrectLength; i += 1) { + a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) ); + } + return a; + }; + + _this.getLengthInBits = function(mode, type) { + + if (1 <= type && type < 10) { + + // 1 - 9 + + switch(mode) { + case QRMode.MODE_NUMBER : return 10; + case QRMode.MODE_ALPHA_NUM : return 9; + case QRMode.MODE_8BIT_BYTE : return 8; + case QRMode.MODE_KANJI : return 8; + default : + throw 'mode:' + mode; + } + + } else if (type < 27) { + + // 10 - 26 + + switch(mode) { + case QRMode.MODE_NUMBER : return 12; + case QRMode.MODE_ALPHA_NUM : return 11; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 10; + default : + throw 'mode:' + mode; + } + + } else if (type < 41) { + + // 27 - 40 + + switch(mode) { + case QRMode.MODE_NUMBER : return 14; + case QRMode.MODE_ALPHA_NUM : return 13; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 12; + default : + throw 'mode:' + mode; + } + + } else { + throw 'type:' + type; + } + }; + + _this.getLostPoint = function(qrcode) { + + var moduleCount = qrcode.getModuleCount(); + + var lostPoint = 0; + + // LEVEL1 + + for (var row = 0; row < moduleCount; row += 1) { + for (var col = 0; col < moduleCount; col += 1) { + + var sameCount = 0; + var dark = qrcode.isDark(row, col); + + for (var r = -1; r <= 1; r += 1) { + + if (row + r < 0 || moduleCount <= row + r) { + continue; + } + + for (var c = -1; c <= 1; c += 1) { + + if (col + c < 0 || moduleCount <= col + c) { + continue; + } + + if (r == 0 && c == 0) { + continue; + } + + if (dark == qrcode.isDark(row + r, col + c) ) { + sameCount += 1; + } + } + } + + if (sameCount > 5) { + lostPoint += (3 + sameCount - 5); + } + } + }; + + // LEVEL2 + + for (var row = 0; row < moduleCount - 1; row += 1) { + for (var col = 0; col < moduleCount - 1; col += 1) { + var count = 0; + if (qrcode.isDark(row, col) ) count += 1; + if (qrcode.isDark(row + 1, col) ) count += 1; + if (qrcode.isDark(row, col + 1) ) count += 1; + if (qrcode.isDark(row + 1, col + 1) ) count += 1; + if (count == 0 || count == 4) { + lostPoint += 3; + } + } + } + + // LEVEL3 + + for (var row = 0; row < moduleCount; row += 1) { + for (var col = 0; col < moduleCount - 6; col += 1) { + if (qrcode.isDark(row, col) + && !qrcode.isDark(row, col + 1) + && qrcode.isDark(row, col + 2) + && qrcode.isDark(row, col + 3) + && qrcode.isDark(row, col + 4) + && !qrcode.isDark(row, col + 5) + && qrcode.isDark(row, col + 6) ) { + lostPoint += 40; + } + } + } + + for (var col = 0; col < moduleCount; col += 1) { + for (var row = 0; row < moduleCount - 6; row += 1) { + if (qrcode.isDark(row, col) + && !qrcode.isDark(row + 1, col) + && qrcode.isDark(row + 2, col) + && qrcode.isDark(row + 3, col) + && qrcode.isDark(row + 4, col) + && !qrcode.isDark(row + 5, col) + && qrcode.isDark(row + 6, col) ) { + lostPoint += 40; + } + } + } + + // LEVEL4 + + var darkCount = 0; + + for (var col = 0; col < moduleCount; col += 1) { + for (var row = 0; row < moduleCount; row += 1) { + if (qrcode.isDark(row, col) ) { + darkCount += 1; + } + } + } + + var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; + lostPoint += ratio * 10; + + return lostPoint; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // QRMath + //--------------------------------------------------------------------- + + var QRMath = function() { + + var EXP_TABLE = new Array(256); + var LOG_TABLE = new Array(256); + + // initialize tables + for (var i = 0; i < 8; i += 1) { + EXP_TABLE[i] = 1 << i; + } + for (var i = 8; i < 256; i += 1) { + EXP_TABLE[i] = EXP_TABLE[i - 4] + ^ EXP_TABLE[i - 5] + ^ EXP_TABLE[i - 6] + ^ EXP_TABLE[i - 8]; + } + for (var i = 0; i < 255; i += 1) { + LOG_TABLE[EXP_TABLE[i] ] = i; + } + + var _this = {}; + + _this.glog = function(n) { + + if (n < 1) { + throw 'glog(' + n + ')'; + } + + return LOG_TABLE[n]; + }; + + _this.gexp = function(n) { + + while (n < 0) { + n += 255; + } + + while (n >= 256) { + n -= 255; + } + + return EXP_TABLE[n]; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // qrPolynomial + //--------------------------------------------------------------------- + + function qrPolynomial(num, shift) { + + if (typeof num.length == 'undefined') { + throw num.length + '/' + shift; + } + + var _num = function() { + var offset = 0; + while (offset < num.length && num[offset] == 0) { + offset += 1; + } + var _num = new Array(num.length - offset + shift); + for (var i = 0; i < num.length - offset; i += 1) { + _num[i] = num[i + offset]; + } + return _num; + }(); + + var _this = {}; + + _this.getAt = function(index) { + return _num[index]; + }; + + _this.getLength = function() { + return _num.length; + }; + + _this.multiply = function(e) { + + var num = new Array(_this.getLength() + e.getLength() - 1); + + for (var i = 0; i < _this.getLength(); i += 1) { + for (var j = 0; j < e.getLength(); j += 1) { + num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) ); + } + } + + return qrPolynomial(num, 0); + }; + + _this.mod = function(e) { + + if (_this.getLength() - e.getLength() < 0) { + return _this; + } + + var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) ); + + var num = new Array(_this.getLength() ); + for (var i = 0; i < _this.getLength(); i += 1) { + num[i] = _this.getAt(i); + } + + for (var i = 0; i < e.getLength(); i += 1) { + num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio); + } + + // recursive call + return qrPolynomial(num, 0).mod(e); + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // QRRSBlock + //--------------------------------------------------------------------- + + var QRRSBlock = function() { + + var RS_BLOCK_TABLE = [ + + // L + // M + // Q + // H + + // 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + + // 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + + // 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + + // 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + + // 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + + // 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + + // 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + + // 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + + // 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + + // 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + + // 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + + // 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + + // 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + + // 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + + // 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12, 7, 37, 13], + + // 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + + // 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + + // 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + + // 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + + // 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + + // 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + + // 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + + // 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + + // 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + + // 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + + // 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + + // 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + + // 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + + // 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + + // 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + + // 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + + // 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + + // 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + + // 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + + // 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + + // 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + + // 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + + // 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + + // 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + + // 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] + ]; + + var qrRSBlock = function(totalCount, dataCount) { + var _this = {}; + _this.totalCount = totalCount; + _this.dataCount = dataCount; + return _this; + }; + + var _this = {}; + + var getRsBlockTable = function(typeNumber, errorCorrectionLevel) { + + switch(errorCorrectionLevel) { + case QRErrorCorrectionLevel.L : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; + case QRErrorCorrectionLevel.M : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; + case QRErrorCorrectionLevel.Q : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; + case QRErrorCorrectionLevel.H : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; + default : + return undefined; + } + }; + + _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) { + + var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel); + + if (typeof rsBlock == 'undefined') { + throw 'bad rs block @ typeNumber:' + typeNumber + + '/errorCorrectionLevel:' + errorCorrectionLevel; + } + + var length = rsBlock.length / 3; + + var list = []; + + for (var i = 0; i < length; i += 1) { + + var count = rsBlock[i * 3 + 0]; + var totalCount = rsBlock[i * 3 + 1]; + var dataCount = rsBlock[i * 3 + 2]; + + for (var j = 0; j < count; j += 1) { + list.push(qrRSBlock(totalCount, dataCount) ); + } + } + + return list; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // qrBitBuffer + //--------------------------------------------------------------------- + + var qrBitBuffer = function() { + + var _buffer = []; + var _length = 0; + + var _this = {}; + + _this.getBuffer = function() { + return _buffer; + }; + + _this.getAt = function(index) { + var bufIndex = Math.floor(index / 8); + return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1; + }; + + _this.put = function(num, length) { + for (var i = 0; i < length; i += 1) { + _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1); + } + }; + + _this.getLengthInBits = function() { + return _length; + }; + + _this.putBit = function(bit) { + + var bufIndex = Math.floor(_length / 8); + if (_buffer.length <= bufIndex) { + _buffer.push(0); + } + + if (bit) { + _buffer[bufIndex] |= (0x80 >>> (_length % 8) ); + } + + _length += 1; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qrNumber + //--------------------------------------------------------------------- + + var qrNumber = function(data) { + + var _mode = QRMode.MODE_NUMBER; + var _data = data; + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return _data.length; + }; + + _this.write = function(buffer) { + + var data = _data; + + var i = 0; + + while (i + 2 < data.length) { + buffer.put(strToNum(data.substring(i, i + 3) ), 10); + i += 3; + } + + if (i < data.length) { + if (data.length - i == 1) { + buffer.put(strToNum(data.substring(i, i + 1) ), 4); + } else if (data.length - i == 2) { + buffer.put(strToNum(data.substring(i, i + 2) ), 7); + } + } + }; + + var strToNum = function(s) { + var num = 0; + for (var i = 0; i < s.length; i += 1) { + num = num * 10 + chatToNum(s.charAt(i) ); + } + return num; + }; + + var chatToNum = function(c) { + if ('0' <= c && c <= '9') { + return c.charCodeAt(0) - '0'.charCodeAt(0); + } + throw 'illegal char :' + c; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qrAlphaNum + //--------------------------------------------------------------------- + + var qrAlphaNum = function(data) { + + var _mode = QRMode.MODE_ALPHA_NUM; + var _data = data; + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return _data.length; + }; + + _this.write = function(buffer) { + + var s = _data; + + var i = 0; + + while (i + 1 < s.length) { + buffer.put( + getCode(s.charAt(i) ) * 45 + + getCode(s.charAt(i + 1) ), 11); + i += 2; + } + + if (i < s.length) { + buffer.put(getCode(s.charAt(i) ), 6); + } + }; + + var getCode = function(c) { + + if ('0' <= c && c <= '9') { + return c.charCodeAt(0) - '0'.charCodeAt(0); + } else if ('A' <= c && c <= 'Z') { + return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10; + } else { + switch (c) { + case ' ' : return 36; + case '$' : return 37; + case '%' : return 38; + case '*' : return 39; + case '+' : return 40; + case '-' : return 41; + case '.' : return 42; + case '/' : return 43; + case ':' : return 44; + default : + throw 'illegal char :' + c; + } + } + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qr8BitByte + //--------------------------------------------------------------------- + + var qr8BitByte = function(data) { + + var _mode = QRMode.MODE_8BIT_BYTE; + var _data = data; + var _bytes = qrcode.stringToBytes(data); + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return _bytes.length; + }; + + _this.write = function(buffer) { + for (var i = 0; i < _bytes.length; i += 1) { + buffer.put(_bytes[i], 8); + } + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qrKanji + //--------------------------------------------------------------------- + + var qrKanji = function(data) { + + var _mode = QRMode.MODE_KANJI; + var _data = data; + + var stringToBytes = qrcode.stringToBytesFuncs['SJIS']; + if (!stringToBytes) { + throw 'sjis not supported.'; + } + !function(c, code) { + // self test for sjis support. + var test = stringToBytes(c); + if (test.length != 2 || ( (test[0] << 8) | test[1]) != code) { + throw 'sjis not supported.'; + } + }('\u53cb', 0x9746); + + var _bytes = stringToBytes(data); + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return ~~(_bytes.length / 2); + }; + + _this.write = function(buffer) { + + var data = _bytes; + + var i = 0; + + while (i + 1 < data.length) { + + var c = ( (0xff & data[i]) << 8) | (0xff & data[i + 1]); + + if (0x8140 <= c && c <= 0x9FFC) { + c -= 0x8140; + } else if (0xE040 <= c && c <= 0xEBBF) { + c -= 0xC140; + } else { + throw 'illegal char at ' + (i + 1) + '/' + c; + } + + c = ( (c >>> 8) & 0xff) * 0xC0 + (c & 0xff); + + buffer.put(c, 13); + + i += 2; + } + + if (i < data.length) { + throw 'illegal char at ' + (i + 1); + } + }; + + return _this; + }; + + //===================================================================== + // GIF Support etc. + // + + //--------------------------------------------------------------------- + // byteArrayOutputStream + //--------------------------------------------------------------------- + + var byteArrayOutputStream = function() { + + var _bytes = []; + + var _this = {}; + + _this.writeByte = function(b) { + _bytes.push(b & 0xff); + }; + + _this.writeShort = function(i) { + _this.writeByte(i); + _this.writeByte(i >>> 8); + }; + + _this.writeBytes = function(b, off, len) { + off = off || 0; + len = len || b.length; + for (var i = 0; i < len; i += 1) { + _this.writeByte(b[i + off]); + } + }; + + _this.writeString = function(s) { + for (var i = 0; i < s.length; i += 1) { + _this.writeByte(s.charCodeAt(i) ); + } + }; + + _this.toByteArray = function() { + return _bytes; + }; + + _this.toString = function() { + var s = ''; + s += '['; + for (var i = 0; i < _bytes.length; i += 1) { + if (i > 0) { + s += ','; + } + s += _bytes[i]; + } + s += ']'; + return s; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // base64EncodeOutputStream + //--------------------------------------------------------------------- + + var base64EncodeOutputStream = function() { + + var _buffer = 0; + var _buflen = 0; + var _length = 0; + var _base64 = ''; + + var _this = {}; + + var writeEncoded = function(b) { + _base64 += String.fromCharCode(encode(b & 0x3f) ); + }; + + var encode = function(n) { + if (n < 0) { + // error. + } else if (n < 26) { + return 0x41 + n; + } else if (n < 52) { + return 0x61 + (n - 26); + } else if (n < 62) { + return 0x30 + (n - 52); + } else if (n == 62) { + return 0x2b; + } else if (n == 63) { + return 0x2f; + } + throw 'n:' + n; + }; + + _this.writeByte = function(n) { + + _buffer = (_buffer << 8) | (n & 0xff); + _buflen += 8; + _length += 1; + + while (_buflen >= 6) { + writeEncoded(_buffer >>> (_buflen - 6) ); + _buflen -= 6; + } + }; + + _this.flush = function() { + + if (_buflen > 0) { + writeEncoded(_buffer << (6 - _buflen) ); + _buffer = 0; + _buflen = 0; + } + + if (_length % 3 != 0) { + // padding + var padlen = 3 - _length % 3; + for (var i = 0; i < padlen; i += 1) { + _base64 += '='; + } + } + }; + + _this.toString = function() { + return _base64; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // base64DecodeInputStream + //--------------------------------------------------------------------- + + var base64DecodeInputStream = function(str) { + + var _str = str; + var _pos = 0; + var _buffer = 0; + var _buflen = 0; + + var _this = {}; + + _this.read = function() { + + while (_buflen < 8) { + + if (_pos >= _str.length) { + if (_buflen == 0) { + return -1; + } + throw 'unexpected end of file./' + _buflen; + } + + var c = _str.charAt(_pos); + _pos += 1; + + if (c == '=') { + _buflen = 0; + return -1; + } else if (c.match(/^\s$/) ) { + // ignore if whitespace. + continue; + } + + _buffer = (_buffer << 6) | decode(c.charCodeAt(0) ); + _buflen += 6; + } + + var n = (_buffer >>> (_buflen - 8) ) & 0xff; + _buflen -= 8; + return n; + }; + + var decode = function(c) { + if (0x41 <= c && c <= 0x5a) { + return c - 0x41; + } else if (0x61 <= c && c <= 0x7a) { + return c - 0x61 + 26; + } else if (0x30 <= c && c <= 0x39) { + return c - 0x30 + 52; + } else if (c == 0x2b) { + return 62; + } else if (c == 0x2f) { + return 63; + } else { + throw 'c:' + c; + } + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // gifImage (B/W) + //--------------------------------------------------------------------- + + var gifImage = function(width, height) { + + var _width = width; + var _height = height; + var _data = new Array(width * height); + + var _this = {}; + + _this.setPixel = function(x, y, pixel) { + _data[y * _width + x] = pixel; + }; + + _this.write = function(out) { + + //--------------------------------- + // GIF Signature + + out.writeString('GIF87a'); + + //--------------------------------- + // Screen Descriptor + + out.writeShort(_width); + out.writeShort(_height); + + out.writeByte(0x80); // 2bit + out.writeByte(0); + out.writeByte(0); + + //--------------------------------- + // Global Color Map + + // black + out.writeByte(0x00); + out.writeByte(0x00); + out.writeByte(0x00); + + // white + out.writeByte(0xff); + out.writeByte(0xff); + out.writeByte(0xff); + + //--------------------------------- + // Image Descriptor + + out.writeString(','); + out.writeShort(0); + out.writeShort(0); + out.writeShort(_width); + out.writeShort(_height); + out.writeByte(0); + + //--------------------------------- + // Local Color Map + + //--------------------------------- + // Raster Data + + var lzwMinCodeSize = 2; + var raster = getLZWRaster(lzwMinCodeSize); + + out.writeByte(lzwMinCodeSize); + + var offset = 0; + + while (raster.length - offset > 255) { + out.writeByte(255); + out.writeBytes(raster, offset, 255); + offset += 255; + } + + out.writeByte(raster.length - offset); + out.writeBytes(raster, offset, raster.length - offset); + out.writeByte(0x00); + + //--------------------------------- + // GIF Terminator + out.writeString(';'); + }; + + var bitOutputStream = function(out) { + + var _out = out; + var _bitLength = 0; + var _bitBuffer = 0; + + var _this = {}; + + _this.write = function(data, length) { + + if ( (data >>> length) != 0) { + throw 'length over'; + } + + while (_bitLength + length >= 8) { + _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) ); + length -= (8 - _bitLength); + data >>>= (8 - _bitLength); + _bitBuffer = 0; + _bitLength = 0; + } + + _bitBuffer = (data << _bitLength) | _bitBuffer; + _bitLength = _bitLength + length; + }; + + _this.flush = function() { + if (_bitLength > 0) { + _out.writeByte(_bitBuffer); + } + }; + + return _this; + }; + + var getLZWRaster = function(lzwMinCodeSize) { + + var clearCode = 1 << lzwMinCodeSize; + var endCode = (1 << lzwMinCodeSize) + 1; + var bitLength = lzwMinCodeSize + 1; + + // Setup LZWTable + var table = lzwTable(); + + for (var i = 0; i < clearCode; i += 1) { + table.add(String.fromCharCode(i) ); + } + table.add(String.fromCharCode(clearCode) ); + table.add(String.fromCharCode(endCode) ); + + var byteOut = byteArrayOutputStream(); + var bitOut = bitOutputStream(byteOut); + + // clear code + bitOut.write(clearCode, bitLength); + + var dataIndex = 0; + + var s = String.fromCharCode(_data[dataIndex]); + dataIndex += 1; + + while (dataIndex < _data.length) { + + var c = String.fromCharCode(_data[dataIndex]); + dataIndex += 1; + + if (table.contains(s + c) ) { + + s = s + c; + + } else { + + bitOut.write(table.indexOf(s), bitLength); + + if (table.size() < 0xfff) { + + if (table.size() == (1 << bitLength) ) { + bitLength += 1; + } + + table.add(s + c); + } + + s = c; + } + } + + bitOut.write(table.indexOf(s), bitLength); + + // end code + bitOut.write(endCode, bitLength); + + bitOut.flush(); + + return byteOut.toByteArray(); + }; + + var lzwTable = function() { + + var _map = {}; + var _size = 0; + + var _this = {}; + + _this.add = function(key) { + if (_this.contains(key) ) { + throw 'dup key:' + key; + } + _map[key] = _size; + _size += 1; + }; + + _this.size = function() { + return _size; + }; + + _this.indexOf = function(key) { + return _map[key]; + }; + + _this.contains = function(key) { + return typeof _map[key] != 'undefined'; + }; + + return _this; + }; + + return _this; + }; + + var createDataURL = function(width, height, getPixel) { + var gif = gifImage(width, height); + for (var y = 0; y < height; y += 1) { + for (var x = 0; x < width; x += 1) { + gif.setPixel(x, y, getPixel(x, y) ); + } + } + + var b = byteArrayOutputStream(); + gif.write(b); + + var base64 = base64EncodeOutputStream(); + var bytes = b.toByteArray(); + for (var i = 0; i < bytes.length; i += 1) { + base64.writeByte(bytes[i]); + } + base64.flush(); + + return 'data:image/gif;base64,' + base64; + }; + + //--------------------------------------------------------------------- + // returns qrcode function. + + return qrcode; + }(); + + // multibyte support + !function() { + + qrcode.stringToBytesFuncs['UTF-8'] = function(s) { + // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array + function toUTF8Array(str) { + var utf8 = []; + for (var i=0; i < str.length; i++) { + var charcode = str.charCodeAt(i); + if (charcode < 0x80) utf8.push(charcode); + else if (charcode < 0x800) { + utf8.push(0xc0 | (charcode >> 6), + 0x80 | (charcode & 0x3f)); + } + else if (charcode < 0xd800 || charcode >= 0xe000) { + utf8.push(0xe0 | (charcode >> 12), + 0x80 | ((charcode>>6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + // surrogate pair + else { + i++; + // UTF-16 encodes 0x10000-0x10FFFF by + // subtracting 0x10000 and splitting the + // 20 bits of 0x0-0xFFFFF into two halves + charcode = 0x10000 + (((charcode & 0x3ff)<<10) + | (str.charCodeAt(i) & 0x3ff)); + utf8.push(0xf0 | (charcode >>18), + 0x80 | ((charcode>>12) & 0x3f), + 0x80 | ((charcode>>6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + } + return utf8; + } + return toUTF8Array(s); + }; + + }(); + + (function (factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } + }(function () { + return qrcode; + })); \ No newline at end of file diff --git a/src/storage/mod.rs b/library/src/storage/mod.rs similarity index 100% rename from src/storage/mod.rs rename to library/src/storage/mod.rs diff --git a/src/storage/stores/in_memory.rs b/library/src/storage/stores/in_memory.rs similarity index 100% rename from src/storage/stores/in_memory.rs rename to library/src/storage/stores/in_memory.rs diff --git a/src/storage/stores/mod.rs b/library/src/storage/stores/mod.rs similarity index 100% rename from src/storage/stores/mod.rs rename to library/src/storage/stores/mod.rs diff --git a/src/storage/stores/sled.rs b/library/src/storage/stores/sled.rs similarity index 100% rename from src/storage/stores/sled.rs rename to library/src/storage/stores/sled.rs diff --git a/src/storage/stores/sqlite.rs b/library/src/storage/stores/sqlite.rs similarity index 100% rename from src/storage/stores/sqlite.rs rename to library/src/storage/stores/sqlite.rs diff --git a/tests/common/mod.rs b/library/tests/common/mod.rs similarity index 100% rename from tests/common/mod.rs rename to library/tests/common/mod.rs diff --git a/tests/integration_tests/block_cache.rs b/library/tests/integration_tests/block_cache.rs similarity index 100% rename from tests/integration_tests/block_cache.rs rename to library/tests/integration_tests/block_cache.rs diff --git a/tests/integration_tests/invoice_tracking.rs b/library/tests/integration_tests/invoice_tracking.rs similarity index 100% rename from tests/integration_tests/invoice_tracking.rs rename to library/tests/integration_tests/invoice_tracking.rs diff --git a/tests/integration_tests/mod.rs b/library/tests/integration_tests/mod.rs similarity index 100% rename from tests/integration_tests/mod.rs rename to library/tests/integration_tests/mod.rs diff --git a/tests/integration_tests/scanning_thread_management.rs b/library/tests/integration_tests/scanning_thread_management.rs similarity index 100% rename from tests/integration_tests/scanning_thread_management.rs rename to library/tests/integration_tests/scanning_thread_management.rs diff --git a/tests/main.rs b/library/tests/main.rs similarity index 100% rename from tests/main.rs rename to library/tests/main.rs diff --git a/tests/rpc_resources/blocks/2477647/block.json b/library/tests/rpc_resources/blocks/2477647/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477647/block.json rename to library/tests/rpc_resources/blocks/2477647/block.json diff --git a/tests/rpc_resources/blocks/2477647/transactions_0.json b/library/tests/rpc_resources/blocks/2477647/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477647/transactions_0.json rename to library/tests/rpc_resources/blocks/2477647/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477647/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477647/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477647/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477647/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477648/block.json b/library/tests/rpc_resources/blocks/2477648/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477648/block.json rename to library/tests/rpc_resources/blocks/2477648/block.json diff --git a/tests/rpc_resources/blocks/2477648/transactions_0.json b/library/tests/rpc_resources/blocks/2477648/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477648/transactions_0.json rename to library/tests/rpc_resources/blocks/2477648/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477648/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477648/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477648/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477648/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477649/block.json b/library/tests/rpc_resources/blocks/2477649/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477649/block.json rename to library/tests/rpc_resources/blocks/2477649/block.json diff --git a/tests/rpc_resources/blocks/2477649/transactions_0.json b/library/tests/rpc_resources/blocks/2477649/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477649/transactions_0.json rename to library/tests/rpc_resources/blocks/2477649/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477649/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477649/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477649/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477649/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477650/block.json b/library/tests/rpc_resources/blocks/2477650/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477650/block.json rename to library/tests/rpc_resources/blocks/2477650/block.json diff --git a/tests/rpc_resources/blocks/2477650/transactions_0.json b/library/tests/rpc_resources/blocks/2477650/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477650/transactions_0.json rename to library/tests/rpc_resources/blocks/2477650/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477650/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477650/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477650/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477650/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477651/block.json b/library/tests/rpc_resources/blocks/2477651/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477651/block.json rename to library/tests/rpc_resources/blocks/2477651/block.json diff --git a/tests/rpc_resources/blocks/2477651/transactions_0.json b/library/tests/rpc_resources/blocks/2477651/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477651/transactions_0.json rename to library/tests/rpc_resources/blocks/2477651/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477651/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477651/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477651/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477651/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477652/block.json b/library/tests/rpc_resources/blocks/2477652/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477652/block.json rename to library/tests/rpc_resources/blocks/2477652/block.json diff --git a/tests/rpc_resources/blocks/2477652/transactions_0.json b/library/tests/rpc_resources/blocks/2477652/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477652/transactions_0.json rename to library/tests/rpc_resources/blocks/2477652/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477652/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477652/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477652/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477652/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477653/block.json b/library/tests/rpc_resources/blocks/2477653/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477653/block.json rename to library/tests/rpc_resources/blocks/2477653/block.json diff --git a/tests/rpc_resources/blocks/2477653/transactions_0.json b/library/tests/rpc_resources/blocks/2477653/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477653/transactions_0.json rename to library/tests/rpc_resources/blocks/2477653/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477653/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477653/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477653/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477653/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477654/block.json b/library/tests/rpc_resources/blocks/2477654/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477654/block.json rename to library/tests/rpc_resources/blocks/2477654/block.json diff --git a/tests/rpc_resources/blocks/2477654/transactions_0.json b/library/tests/rpc_resources/blocks/2477654/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477654/transactions_0.json rename to library/tests/rpc_resources/blocks/2477654/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477654/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477654/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477654/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477654/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477655/block.json b/library/tests/rpc_resources/blocks/2477655/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477655/block.json rename to library/tests/rpc_resources/blocks/2477655/block.json diff --git a/tests/rpc_resources/blocks/2477655/transactions_0.json b/library/tests/rpc_resources/blocks/2477655/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477655/transactions_0.json rename to library/tests/rpc_resources/blocks/2477655/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477655/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477655/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477655/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477655/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477656/block.json b/library/tests/rpc_resources/blocks/2477656/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477656/block.json rename to library/tests/rpc_resources/blocks/2477656/block.json diff --git a/tests/rpc_resources/blocks/2477656/daemon_height.json b/library/tests/rpc_resources/blocks/2477656/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477656/daemon_height.json rename to library/tests/rpc_resources/blocks/2477656/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477656/transactions_0.json b/library/tests/rpc_resources/blocks/2477656/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477656/transactions_0.json rename to library/tests/rpc_resources/blocks/2477656/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477656/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477656/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477656/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477656/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477657/block.json b/library/tests/rpc_resources/blocks/2477657/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477657/block.json rename to library/tests/rpc_resources/blocks/2477657/block.json diff --git a/tests/rpc_resources/blocks/2477657/daemon_height.json b/library/tests/rpc_resources/blocks/2477657/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477657/daemon_height.json rename to library/tests/rpc_resources/blocks/2477657/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477657/transactions_0.json b/library/tests/rpc_resources/blocks/2477657/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477657/transactions_0.json rename to library/tests/rpc_resources/blocks/2477657/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477657/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477657/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477657/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477657/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477657_alt/block.json b/library/tests/rpc_resources/blocks/2477657_alt/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477657_alt/block.json rename to library/tests/rpc_resources/blocks/2477657_alt/block.json diff --git a/tests/rpc_resources/blocks/2477657_alt/daemon_height.json b/library/tests/rpc_resources/blocks/2477657_alt/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477657_alt/daemon_height.json rename to library/tests/rpc_resources/blocks/2477657_alt/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477657_alt/transactions_0.json b/library/tests/rpc_resources/blocks/2477657_alt/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477657_alt/transactions_0.json rename to library/tests/rpc_resources/blocks/2477657_alt/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477657_alt/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477657_alt/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477657_alt/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477657_alt/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477658/block.json b/library/tests/rpc_resources/blocks/2477658/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477658/block.json rename to library/tests/rpc_resources/blocks/2477658/block.json diff --git a/tests/rpc_resources/blocks/2477658/daemon_height.json b/library/tests/rpc_resources/blocks/2477658/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477658/daemon_height.json rename to library/tests/rpc_resources/blocks/2477658/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477658/transactions_0.json b/library/tests/rpc_resources/blocks/2477658/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477658/transactions_0.json rename to library/tests/rpc_resources/blocks/2477658/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477658/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477658/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477658/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477658/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477658_alt/block.json b/library/tests/rpc_resources/blocks/2477658_alt/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477658_alt/block.json rename to library/tests/rpc_resources/blocks/2477658_alt/block.json diff --git a/tests/rpc_resources/blocks/2477658_alt/daemon_height.json b/library/tests/rpc_resources/blocks/2477658_alt/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477658_alt/daemon_height.json rename to library/tests/rpc_resources/blocks/2477658_alt/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477658_alt/transactions_0.json b/library/tests/rpc_resources/blocks/2477658_alt/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477658_alt/transactions_0.json rename to library/tests/rpc_resources/blocks/2477658_alt/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477658_alt/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477658_alt/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477658_alt/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477658_alt/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477659/block.json b/library/tests/rpc_resources/blocks/2477659/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477659/block.json rename to library/tests/rpc_resources/blocks/2477659/block.json diff --git a/tests/rpc_resources/blocks/2477659/daemon_height.json b/library/tests/rpc_resources/blocks/2477659/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477659/daemon_height.json rename to library/tests/rpc_resources/blocks/2477659/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477659/transactions_0.json b/library/tests/rpc_resources/blocks/2477659/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477659/transactions_0.json rename to library/tests/rpc_resources/blocks/2477659/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477659/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477659/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477659/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477659/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477660/block.json b/library/tests/rpc_resources/blocks/2477660/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477660/block.json rename to library/tests/rpc_resources/blocks/2477660/block.json diff --git a/tests/rpc_resources/blocks/2477660/daemon_height.json b/library/tests/rpc_resources/blocks/2477660/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477660/daemon_height.json rename to library/tests/rpc_resources/blocks/2477660/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477660/transactions_0.json b/library/tests/rpc_resources/blocks/2477660/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477660/transactions_0.json rename to library/tests/rpc_resources/blocks/2477660/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477660/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477660/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477660/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477660/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477661/block.json b/library/tests/rpc_resources/blocks/2477661/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477661/block.json rename to library/tests/rpc_resources/blocks/2477661/block.json diff --git a/tests/rpc_resources/blocks/2477661/daemon_height.json b/library/tests/rpc_resources/blocks/2477661/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477661/daemon_height.json rename to library/tests/rpc_resources/blocks/2477661/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477661/transactions_0.json b/library/tests/rpc_resources/blocks/2477661/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477661/transactions_0.json rename to library/tests/rpc_resources/blocks/2477661/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477661/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477661/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477661/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477661/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477662/block.json b/library/tests/rpc_resources/blocks/2477662/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477662/block.json rename to library/tests/rpc_resources/blocks/2477662/block.json diff --git a/tests/rpc_resources/blocks/2477662/daemon_height.json b/library/tests/rpc_resources/blocks/2477662/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477662/daemon_height.json rename to library/tests/rpc_resources/blocks/2477662/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477662/transactions_0.json b/library/tests/rpc_resources/blocks/2477662/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477662/transactions_0.json rename to library/tests/rpc_resources/blocks/2477662/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477662/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477662/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477662/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477662/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477663/block.json b/library/tests/rpc_resources/blocks/2477663/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477663/block.json rename to library/tests/rpc_resources/blocks/2477663/block.json diff --git a/tests/rpc_resources/blocks/2477663/daemon_height.json b/library/tests/rpc_resources/blocks/2477663/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477663/daemon_height.json rename to library/tests/rpc_resources/blocks/2477663/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477663/transactions_0.json b/library/tests/rpc_resources/blocks/2477663/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477663/transactions_0.json rename to library/tests/rpc_resources/blocks/2477663/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477663/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477663/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477663/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477663/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477664/block.json b/library/tests/rpc_resources/blocks/2477664/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477664/block.json rename to library/tests/rpc_resources/blocks/2477664/block.json diff --git a/tests/rpc_resources/blocks/2477664/daemon_height.json b/library/tests/rpc_resources/blocks/2477664/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477664/daemon_height.json rename to library/tests/rpc_resources/blocks/2477664/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477664/transactions_0.json b/library/tests/rpc_resources/blocks/2477664/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477664/transactions_0.json rename to library/tests/rpc_resources/blocks/2477664/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477664/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477664/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477664/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477664/txs_hashes_0.json diff --git a/tests/rpc_resources/blocks/2477665/block.json b/library/tests/rpc_resources/blocks/2477665/block.json similarity index 100% rename from tests/rpc_resources/blocks/2477665/block.json rename to library/tests/rpc_resources/blocks/2477665/block.json diff --git a/tests/rpc_resources/blocks/2477665/daemon_height.json b/library/tests/rpc_resources/blocks/2477665/daemon_height.json similarity index 100% rename from tests/rpc_resources/blocks/2477665/daemon_height.json rename to library/tests/rpc_resources/blocks/2477665/daemon_height.json diff --git a/tests/rpc_resources/blocks/2477665/transactions_0.json b/library/tests/rpc_resources/blocks/2477665/transactions_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477665/transactions_0.json rename to library/tests/rpc_resources/blocks/2477665/transactions_0.json diff --git a/tests/rpc_resources/blocks/2477665/txs_hashes_0.json b/library/tests/rpc_resources/blocks/2477665/txs_hashes_0.json similarity index 100% rename from tests/rpc_resources/blocks/2477665/txs_hashes_0.json rename to library/tests/rpc_resources/blocks/2477665/txs_hashes_0.json diff --git a/tests/rpc_resources/transactions/hashes_with_payment.json b/library/tests/rpc_resources/transactions/hashes_with_payment.json similarity index 100% rename from tests/rpc_resources/transactions/hashes_with_payment.json rename to library/tests/rpc_resources/transactions/hashes_with_payment.json diff --git a/tests/rpc_resources/transactions/hashes_with_payment_2.json b/library/tests/rpc_resources/transactions/hashes_with_payment_2.json similarity index 100% rename from tests/rpc_resources/transactions/hashes_with_payment_2.json rename to library/tests/rpc_resources/transactions/hashes_with_payment_2.json diff --git a/tests/rpc_resources/transactions/hashes_with_payment_account_0.json b/library/tests/rpc_resources/transactions/hashes_with_payment_account_0.json similarity index 100% rename from tests/rpc_resources/transactions/hashes_with_payment_account_0.json rename to library/tests/rpc_resources/transactions/hashes_with_payment_account_0.json diff --git a/tests/rpc_resources/transactions/hashes_with_payment_timelock.json b/library/tests/rpc_resources/transactions/hashes_with_payment_timelock.json similarity index 100% rename from tests/rpc_resources/transactions/hashes_with_payment_timelock.json rename to library/tests/rpc_resources/transactions/hashes_with_payment_timelock.json diff --git a/tests/rpc_resources/transactions/txs_with_payment.json b/library/tests/rpc_resources/transactions/txs_with_payment.json similarity index 100% rename from tests/rpc_resources/transactions/txs_with_payment.json rename to library/tests/rpc_resources/transactions/txs_with_payment.json diff --git a/tests/rpc_resources/transactions/txs_with_payment_2.json b/library/tests/rpc_resources/transactions/txs_with_payment_2.json similarity index 100% rename from tests/rpc_resources/transactions/txs_with_payment_2.json rename to library/tests/rpc_resources/transactions/txs_with_payment_2.json diff --git a/tests/rpc_resources/transactions/txs_with_payment_account_0.json b/library/tests/rpc_resources/transactions/txs_with_payment_account_0.json similarity index 100% rename from tests/rpc_resources/transactions/txs_with_payment_account_0.json rename to library/tests/rpc_resources/transactions/txs_with_payment_account_0.json diff --git a/tests/rpc_resources/transactions/txs_with_payment_timelock.json b/library/tests/rpc_resources/transactions/txs_with_payment_timelock.json similarity index 100% rename from tests/rpc_resources/transactions/txs_with_payment_timelock.json rename to library/tests/rpc_resources/transactions/txs_with_payment_timelock.json diff --git a/tests/rpc_resources/txpools/hashes.json b/library/tests/rpc_resources/txpools/hashes.json similarity index 100% rename from tests/rpc_resources/txpools/hashes.json rename to library/tests/rpc_resources/txpools/hashes.json diff --git a/tests/rpc_resources/txpools/hashes_with_payment.json b/library/tests/rpc_resources/txpools/hashes_with_payment.json similarity index 100% rename from tests/rpc_resources/txpools/hashes_with_payment.json rename to library/tests/rpc_resources/txpools/hashes_with_payment.json diff --git a/tests/rpc_resources/txpools/hashes_with_payment_2.json b/library/tests/rpc_resources/txpools/hashes_with_payment_2.json similarity index 100% rename from tests/rpc_resources/txpools/hashes_with_payment_2.json rename to library/tests/rpc_resources/txpools/hashes_with_payment_2.json diff --git a/tests/rpc_resources/txpools/hashes_with_payment_account_0.json b/library/tests/rpc_resources/txpools/hashes_with_payment_account_0.json similarity index 100% rename from tests/rpc_resources/txpools/hashes_with_payment_account_0.json rename to library/tests/rpc_resources/txpools/hashes_with_payment_account_0.json diff --git a/tests/rpc_resources/txpools/hashes_with_payment_timelock.json b/library/tests/rpc_resources/txpools/hashes_with_payment_timelock.json similarity index 100% rename from tests/rpc_resources/txpools/hashes_with_payment_timelock.json rename to library/tests/rpc_resources/txpools/hashes_with_payment_timelock.json diff --git a/tests/rpc_resources/txpools/txpool.json b/library/tests/rpc_resources/txpools/txpool.json similarity index 100% rename from tests/rpc_resources/txpools/txpool.json rename to library/tests/rpc_resources/txpools/txpool.json diff --git a/tests/rpc_resources/txpools/txpool_with_payment.json b/library/tests/rpc_resources/txpools/txpool_with_payment.json similarity index 100% rename from tests/rpc_resources/txpools/txpool_with_payment.json rename to library/tests/rpc_resources/txpools/txpool_with_payment.json diff --git a/tests/rpc_resources/txpools/txpool_with_payment_2.json b/library/tests/rpc_resources/txpools/txpool_with_payment_2.json similarity index 100% rename from tests/rpc_resources/txpools/txpool_with_payment_2.json rename to library/tests/rpc_resources/txpools/txpool_with_payment_2.json diff --git a/tests/rpc_resources/txpools/txpool_with_payment_account_0.json b/library/tests/rpc_resources/txpools/txpool_with_payment_account_0.json similarity index 100% rename from tests/rpc_resources/txpools/txpool_with_payment_account_0.json rename to library/tests/rpc_resources/txpools/txpool_with_payment_account_0.json diff --git a/tests/rpc_resources/txpools/txpool_with_payment_timelock.json b/library/tests/rpc_resources/txpools/txpool_with_payment_timelock.json similarity index 100% rename from tests/rpc_resources/txpools/txpool_with_payment_timelock.json rename to library/tests/rpc_resources/txpools/txpool_with_payment_timelock.json diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000..1aeada9 --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "acceptxmr-server" +publish = false +version = "0.1.0" +edition = "2021" +rust-version = "1.65" +license = "MIT OR Apache-2.0" +description = "A monero payment gateway." +repository = "https://github.com/busyboredom/acceptxmr" +readme = "README.md" +keywords = ["crypto", "gateway", "monero", "payment", "xmr"] +categories = ["cryptography::cryptocurrencies"] + +[[bin]] +name = "acceptxmr-server" +path = "src/main.rs" + +[dependencies] +acceptxmr = { path = "../library", features = ["serde", "sqlite"] } +actix = "0.13" +actix-files = "0.6" +actix-session = { version = "0.7", features = ["cookie-session"] } +actix-web = "4" +actix-web-actors = "4" +bytestring = "1" +env_logger = "0.10" +log = "0.4" +rand = "0.8" +rand_chacha = "0.3" +serde = {version = "1", features = ["derive"] } +serde_json = "1" diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..3cdbd60 --- /dev/null +++ b/server/README.md @@ -0,0 +1,18 @@ +[![BuildStatus](https://github.com/busyboredom/acceptxmr/workflows/CI/badge.svg)](https://img.shields.io/github/actions/workflow/status/busyboredom/acceptxmr/ci.yml?branch=main) + +# `AcceptXMR-Server`: A monero payment gateway. +`AcceptXMR-Server` is a batteries-included monero payment gateway built around +the AcceptXMR library. + +If your application requires more flexibility than `AcceptXMR-Server` offers, +please see the [`AcceptXMR`](../library/) library instead. + +## Getting Started +### Running with Docker +TODO + +### Configuration +TODO + +### API +TODO diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000..f8d491c --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,312 @@ +use std::{ + future::Future, + pin::Pin, + task::Poll, + time::{Duration, Instant}, +}; + +use acceptxmr::{ + storage::stores::Sqlite, Invoice, InvoiceId, PaymentGateway, PaymentGatewayBuilder, Subscriber, +}; +use actix::{prelude::Stream, Actor, ActorContext, AsyncContext, StreamHandler}; +use actix_files::Files; +use actix_session::{ + config::CookieContentSecurity, storage::CookieSessionStore, Session, SessionMiddleware, +}; +use actix_web::{ + cookie, get, + http::header::{CacheControl, CacheDirective}, + post, web, + web::Data, + App, HttpRequest, HttpResponse, HttpServer, +}; +use actix_web_actors::ws; +use bytestring::ByteString; +use log::{debug, error, info, warn, LevelFilter}; +use rand::{thread_rng, Rng}; +use serde::Deserialize; +use serde_json::json; + +/// Time before lack of client response causes a timeout. +const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); +/// Time between sending heartbeat pings. +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(1); +/// Length of secure session key for cookies. +const SESSION_KEY_LEN: usize = 64; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::builder() + .filter_level(LevelFilter::Warn) + .filter_module("acceptxmr", log::LevelFilter::Debug) + .filter_module("websockets", log::LevelFilter::Trace) + .init(); + + // The private view key should be stored securely outside of the git repository. + // It is hardcoded here for demonstration purposes only. + let private_view_key = "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; + // No need to keep the primary address secret. + let primary_address = "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; + + let invoice_store = + Sqlite::new("AcceptXMR_DB", "invoices").expect("failed to open invoice store"); + let payment_gateway = PaymentGatewayBuilder::new( + private_view_key.to_string(), + primary_address.to_string(), + invoice_store, + ) + .daemon_url("http://xmr-node.cakewallet.com:18081".to_string()) + .build() + .expect("failed to build payment gateway"); + info!("Payment gateway created."); + + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + info!("Payment gateway running."); + + // Watch for invoice updates and deal with them accordingly. + let gateway_copy = payment_gateway.clone(); + std::thread::spawn(move || { + // Watch all invoice updates. + let mut subscriber = gateway_copy.subscribe_all(); + loop { + let invoice = match subscriber.blocking_recv() { + Some(p) => p, + // Global subscriptions should not close. + None => panic!("Blockchain scanner crashed!"), + }; + // If it's confirmed or expired, we probably shouldn't bother tracking it + // anymore. + if (invoice.is_confirmed() && invoice.creation_height() < invoice.current_height()) + || invoice.is_expired() + { + debug!( + "Invoice to index {} is either confirmed or expired. Removing invoice now", + invoice.index() + ); + if let Err(e) = gateway_copy.remove_invoice(invoice.id()) { + error!("Failed to remove fully confirmed invoice: {}", e); + }; + } + } + }); + + // Create secure session key for cookies. + let mut key_arr = [0u8; SESSION_KEY_LEN]; + thread_rng().fill(&mut key_arr[..]); + let session_key = cookie::Key::generate(); + + // Run the demo webpage. + let shared_payment_gateway = Data::new(payment_gateway); + HttpServer::new(move || { + App::new() + .wrap( + SessionMiddleware::builder(CookieSessionStore::default(), session_key.clone()) + .cookie_name("acceptxmr_session".to_string()) + .cookie_secure(false) + .cookie_content_security(CookieContentSecurity::Private) + .build(), + ) + .app_data(shared_payment_gateway.clone()) + .service(update) + .service(checkout) + .service(websocket) + .service(Files::new("", "./server/static").index_file("index.html")) + }) + .bind("0.0.0.0:8080")? + .run() + .await +} + +#[derive(Deserialize)] +struct CheckoutInfo { + message: String, +} + +/// Create new invoice and place cookie. +#[post("/checkout")] +async fn checkout( + session: Session, + checkout_info: web::Json, + payment_gateway: web::Data>, +) -> Result { + let invoice_id = payment_gateway + .new_invoice(1_000_000_000, 2, 5, checkout_info.message.clone()) + .unwrap(); + session.insert("id", invoice_id)?; + Ok(HttpResponse::Ok() + .append_header(CacheControl(vec![CacheDirective::NoStore])) + .finish()) +} + +// Get invoice update without waiting for websocket. +#[get("/update")] +async fn update( + session: Session, + payment_gateway: web::Data>, +) -> Result { + if let Ok(Some(invoice_id)) = session.get::("id") { + if let Ok(Some(invoice)) = payment_gateway.get_invoice(invoice_id) { + return Ok(HttpResponse::Ok() + .append_header(CacheControl(vec![CacheDirective::NoStore])) + .json(json!( + { + "address": invoice.address(), + "amount_paid": invoice.amount_paid(), + "amount_requested": invoice.amount_requested(), + "uri": invoice.uri(), + "confirmations": invoice.confirmations(), + "confirmations_required": invoice.confirmations_required(), + "expiration_in": invoice.expiration_in(), + } + ))); + }; + } + Ok(HttpResponse::Gone() + .append_header(CacheControl(vec![CacheDirective::NoStore])) + .finish()) +} + +/// WebSocket rout. +#[get("/ws/")] +async fn websocket( + session: Session, + req: HttpRequest, + stream: web::Payload, + payment_gateway: web::Data>, +) -> Result { + let invoice_id = match session.get::("id") { + Ok(Some(i)) => i, + _ => { + return Ok(HttpResponse::NotFound() + .append_header(CacheControl(vec![CacheDirective::NoStore])) + .finish()) + } + }; + let subscriber = match payment_gateway.subscribe(invoice_id) { + Some(s) => s, + _ => { + return Ok(HttpResponse::NotFound() + .append_header(CacheControl(vec![CacheDirective::NoStore])) + .finish()) + } + }; + let websocket = WebSocket::new(subscriber); + ws::start(websocket, &req, stream) +} + +/// Define websocket HTTP actor +struct WebSocket { + last_heartbeat: Instant, + invoice_subscriber: Option, +} + +impl WebSocket { + fn new(invoice_subscriber: Subscriber) -> Self { + Self { + last_heartbeat: Instant::now(), + invoice_subscriber: Some(invoice_subscriber), + } + } + + /// Sends ping to client every `HEARTBEAT_INTERVAL` and checks for responses + /// from client + fn heartbeat(&self, ctx: &mut ::Context) { + ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { + // check client heartbeats + if Instant::now().duration_since(act.last_heartbeat) > CLIENT_TIMEOUT { + // heartbeat timed out + warn!("Websocket Client heartbeat failed, disconnecting!"); + ctx.stop(); + return; + } + ctx.ping(b""); + }); + } +} + +impl Actor for WebSocket { + type Context = ws::WebsocketContext; + + /// This method is called on actor start. We add the invoice subscriber as a + /// stream here, and start heartbeat checks as well. + fn started(&mut self, ctx: &mut Self::Context) { + if let Some(subscriber) = self.invoice_subscriber.take() { + >::add_stream(InvoiceStream(subscriber), ctx); + } + self.heartbeat(ctx); + } +} + +/// Handle incoming websocket messages. +impl StreamHandler> for WebSocket { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + match msg { + Ok(ws::Message::Pong(_)) => { + self.last_heartbeat = Instant::now(); + } + Ok(ws::Message::Close(reason)) => { + match &reason { + Some(r) => debug!("Websocket client closing: {:#?}", r.description), + None => debug!("Websocket client closing"), + } + ctx.close(reason); + ctx.stop(); + } + Ok(m) => debug!("Received unexpected message from websocket client: {:?}", m), + Err(e) => warn!("Received error from websocket client: {:?}", e), + } + } +} + +/// Handle incoming invoice updates. +impl StreamHandler for WebSocket { + fn handle(&mut self, invoice_update: Invoice, ctx: &mut Self::Context) { + // Send the update to the user. + ctx.text(ByteString::from( + json!( + { + "address": invoice_update.address(), + "amount_paid": invoice_update.amount_paid(), + "amount_requested": invoice_update.amount_requested(), + "uri": invoice_update.uri(), + "confirmations": invoice_update.confirmations(), + "confirmations_required": invoice_update.confirmations_required(), + "expiration_in": invoice_update.expiration_in(), + } + ) + .to_string(), + )); + // If the invoice is confirmed or expired, stop checking for updates. + if invoice_update.is_confirmed() { + ctx.close(Some(ws::CloseReason::from(( + ws::CloseCode::Normal, + "Invoice Complete", + )))); + ctx.stop(); + } else if invoice_update.is_expired() { + ctx.close(Some(ws::CloseReason::from(( + ws::CloseCode::Normal, + "Invoice Expired", + )))); + ctx.stop(); + } + } +} + +// Wrapping `Subscriber` and implementing `Stream` on the wrapper allows us to +// use it as an efficient asynchronous stream for the Actix websocket. +struct InvoiceStream(Subscriber); + +impl Stream for InvoiceStream { + type Item = Invoice; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll(cx) + } +} diff --git a/server/static/acceptxmr.css b/server/static/acceptxmr.css new file mode 100644 index 0000000..0880322 --- /dev/null +++ b/server/static/acceptxmr.css @@ -0,0 +1,151 @@ +body { + background-color: #4d4d4d; +} + +.acceptxmr { + max-width: 80ch; + margin: auto; + background-color: #f26822; + border-radius: 1em; + display: flex; + flex-flow: column wrap; + align-items: center; +} + +.acceptxmr button { + background-color: #0088ff; + border-radius: 0.5em; + border: 0; + margin: 0; + font-size: medium; +} + +.acceptxmr button:hover { + background-color: #005fb3; + cursor: pointer; +} + +.acceptxmr button:disabled { + background-color: #979797; +} + +.acceptxmr #message-container { + margin: 1em; + flex: 2 1 20em; +} + +.acceptxmr #payment-content { + display: none; +} + +.acceptxmr .content button { + margin: 1em; + flex: 1 2 10ch; + padding: 12px 0.5em; + align-self: flex-end; +} + +.acceptxmr textarea { + border-radius: 0.5em; + border: 0; + padding: 0.5em; + width: 100%; + margin: 0; + background-color: #4d4d4d; + color: #ffffff; + resize: vertical; +} + +.acceptxmr .instruction-container { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; +} + +.acceptxmr .instruction-container p { + margin: 0.1em; + font-size: large; +} + +.acceptxmr .instruction-container button { + margin: 0.5em; + padding: 0.25em 0.5em; +} + +.acceptxmr .warning { + animation: acceptxmr-fade 2s linear; + animation-iteration-count: infinite; +} + +@keyframes acceptxmr-fade { + 0%,100% { opacity: 0.2 } + 50% { opacity: 1 } +} + +.acceptxmr hr { + width: 90%; + border-style: solid; + border-color: #000000; + margin: 0; +} + +.content { + width: 100%; + display: flex; + flex-flow: row wrap; +} + +.acceptxmr .qrcode-container { + flex: 1 1 10ch; + margin: 1em; + display: flex; + justify-content: center; +} + +.acceptxmr .qrcode-container svg { + border-radius: 1em; + min-width: 18ch; + align-self: center; + background-color: #dddddd; +} + +.acceptxmr .details { + flex: 2 2 30ch; + margin: 0 1em 1em 1em; + overflow: hidden; + align-self: center; +} + +.acceptxmr #address-container { + display: flex; + height: 3em; + border-radius: 0.5em; + margin-bottom: 0.5em; + background-color: #ffffff; + justify-content: space-between; +} + +.acceptxmr #address-container p { + overflow-x: scroll; + overflow-y: hidden; + margin: 0 0.5em 0 0.5em; + height: 100%; + width: 100%; + display: flex; + align-items: center; +} + +.acceptxmr #address-container button { + border-radius: 0 0.5em 0.5em 0; + height: 100%; + padding: 0 1em; + margin: 0; +} + +.acceptxmr .status { + margin: 0 0 0 0; + border-radius: 0.5em; + padding: 0.5em; + background-color: #ffffff; +} diff --git a/server/static/acceptxmr.js b/server/static/acceptxmr.js new file mode 100644 index 0000000..ff24d34 --- /dev/null +++ b/server/static/acceptxmr.js @@ -0,0 +1,152 @@ +// Try to load existing invoice on page load. +async function init() { + let response = await fetch("/update"); + if (response.status !== 410 ) { + let invoiceUpdate = await response.json(); + displayInvoiceUpdate(invoiceUpdate); + await next(true); + } +} +init() + +async function next(hasAddress) { + // Hide prep stuff, show payment stuff. + document.getElementById("preperation-content").style.display = "None"; + document.getElementById("payment-content").style.display = "inherit"; + + // Create invoice. + if (!hasAddress) { + document.getElementById("instruction").innerHTML = "Loading..."; + await newAddress(); + } else { + newWebsocket(); + } +} + +async function newAddress() { + const message = document.getElementById("message").value; + const checkOutInfo = { + method: "POST", + body: JSON.stringify({ + "message": message + }), + headers: { + 'content-type': 'application/json' + } + }; + + await fetch("/checkout", checkOutInfo); + newWebsocket(); + + let response = await fetch("/update"); + let invoiceUpdate = await response.json(); + displayInvoiceUpdate(invoiceUpdate); +} + +function displayInvoiceUpdate(invoiceUpdate) { + console.log(invoiceUpdate); + + // Show paid/due. + document.getElementById("paid").innerHTML = picoToXMR(invoiceUpdate.amount_paid); + document.getElementById("due").innerHTML = picoToXMR(invoiceUpdate.amount_requested); + + // Show confirmations/required. + var confirmations = invoiceUpdate.confirmations; + document.getElementById("confirmations").innerHTML = Math.max(0, confirmations); + document.getElementById("confirmations-required").innerHTML = invoiceUpdate.confirmations_required; + + // Show instructive text depending on invoice state. + var instructionString = "Loading..."; + var instructionClass = "acceptxmr-instruction"; + var newAddressBtnHidden = true; + var closeReason = null; + if (confirmations !== null && confirmations >= invoiceUpdate.confirmations_required) { + instructionString = "Paid! Thank you" + closeReason = "Confirmed"; + } else if (invoiceUpdate.amount_paid >= invoiceUpdate.amount_requested) { + instructionString = "Paid! Waiting for Confirmation..." + } else if (invoiceUpdate.expiration_in > 2) { + instructionString = "Send Monero to Address Below" + } else if (invoiceUpdate.expiration_in > 0) { + instructionString = "Address Expiring Soon"; + instructionClass += " warning"; + newAddressBtnHidden = false; + } else { + instructionString = "Address Expired!"; + newAddressBtnHidden = false; + closeReason = "Expired"; + } + document.getElementById("instruction").innerHTML = instructionString; + document.getElementById("instruction").classList = instructionClass; + + // Hide address if nearing expiration. + document.getElementById("new-address-btn").hidden = newAddressBtnHidden; + document.getElementById("address-copy-btn").disabled = !newAddressBtnHidden; + if (newAddressBtnHidden) { + document.getElementById("address").innerHTML = invoiceUpdate.address; + + const qr = qrcode(0, "M"); + qr.addData(invoiceUpdate.uri); + qr.make(); + document.getElementById('qrcode-container').innerHTML = qr.createSvgTag({ scalable: true }); + } else { + document.getElementById("address").innerHTML = "Expiring or expired..."; + document.getElementById('qrcode-container').innerHTML = ""; + } + + return closeReason; +} + +function newWebsocket() { + // Close websocket if it already exists. + if (typeof window.acceptxmrSocket != 'undefined') { + window.acceptxmrSocket.close(1000, "New Address"); + } + + // Open websocket. + window.acceptxmrSocket = new WebSocket("ws://localhost:8080/ws/"); + + window.acceptxmrSocket.onmessage = function (event) { + let closeReason = displayInvoiceUpdate(JSON.parse(event.data)); + if (closeReason != null) { + window.acceptxmrSocket.close(1000, closeReason); + } + } + + // If the websocket closes cleanly, log it. Otherwise, alert the user. + window.acceptxmrSocket.onclose = function (event) { + if (event.code === 1000) { + console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); + } else { + // Server process killed or network down. + // Event.code is usually 1006 in this case. + alert('Connection died. If you have paid already, rest assured that it will still be processed.'); + } + }; + + window.acceptxmrSocket.onerror = function (error) { + alert(`[error] ${error.message}`); + }; +} + +// Convert from piconeros to monero. +function picoToXMR(amount) { + const divisor = 1_000_000_000_000; + const xmr = Math.floor(amount / divisor) + amount % divisor / divisor; + return new Intl.NumberFormat(undefined, { maximumSignificantDigits: 20 }).format(xmr); +} + +// Make the copy button work. +function copyInvoiceAddress() { + // Get the text field + const copyText = document.getElementById("address"); + + // Copy the text inside the text field + navigator.clipboard.writeText(copyText.innerHTML); + + // Provide feedback + document.getElementById("address-copy-btn").innerHTML = "Copied!"; + setTimeout(function () { + document.getElementById("address-copy-btn").innerHTML = "Copy"; + }, 1000); +} diff --git a/server/static/favicon.ico b/server/static/favicon.ico new file mode 100644 index 0000000..37a6dae Binary files /dev/null and b/server/static/favicon.ico differ diff --git a/server/static/index.html b/server/static/index.html new file mode 100644 index 0000000..b9511dd --- /dev/null +++ b/server/static/index.html @@ -0,0 +1,40 @@ + + +AcceptXMR Websocket Demo + +
+
+

AcceptXMR Websocket Demo

+ +
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+

+ +
+ +

+ Paid: / 0.00000000 XMR
+ Confirmations: + 0 / 0
+

+
+
+
+ + + diff --git a/server/static/vendor/qrcode.js b/server/static/vendor/qrcode.js new file mode 100644 index 0000000..fde008b --- /dev/null +++ b/server/static/vendor/qrcode.js @@ -0,0 +1,2297 @@ +//--------------------------------------------------------------------- +// +// QR Code Generator for JavaScript +// +// Copyright (c) 2009 Kazuhiko Arase +// +// URL: http://www.d-project.com/ +// +// Licensed under the MIT license: +// http://www.opensource.org/licenses/mit-license.php +// +// The word 'QR Code' is registered trademark of +// DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/faqpatent-e.html +// +//--------------------------------------------------------------------- + +var qrcode = function() { + + //--------------------------------------------------------------------- + // qrcode + //--------------------------------------------------------------------- + + /** + * qrcode + * @param typeNumber 1 to 40 + * @param errorCorrectionLevel 'L','M','Q','H' + */ + var qrcode = function(typeNumber, errorCorrectionLevel) { + + var PAD0 = 0xEC; + var PAD1 = 0x11; + + var _typeNumber = typeNumber; + var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel]; + var _modules = null; + var _moduleCount = 0; + var _dataCache = null; + var _dataList = []; + + var _this = {}; + + var makeImpl = function(test, maskPattern) { + + _moduleCount = _typeNumber * 4 + 17; + _modules = function(moduleCount) { + var modules = new Array(moduleCount); + for (var row = 0; row < moduleCount; row += 1) { + modules[row] = new Array(moduleCount); + for (var col = 0; col < moduleCount; col += 1) { + modules[row][col] = null; + } + } + return modules; + }(_moduleCount); + + setupPositionProbePattern(0, 0); + setupPositionProbePattern(_moduleCount - 7, 0); + setupPositionProbePattern(0, _moduleCount - 7); + setupPositionAdjustPattern(); + setupTimingPattern(); + setupTypeInfo(test, maskPattern); + + if (_typeNumber >= 7) { + setupTypeNumber(test); + } + + if (_dataCache == null) { + _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList); + } + + mapData(_dataCache, maskPattern); + }; + + var setupPositionProbePattern = function(row, col) { + + for (var r = -1; r <= 7; r += 1) { + + if (row + r <= -1 || _moduleCount <= row + r) continue; + + for (var c = -1; c <= 7; c += 1) { + + if (col + c <= -1 || _moduleCount <= col + c) continue; + + if ( (0 <= r && r <= 6 && (c == 0 || c == 6) ) + || (0 <= c && c <= 6 && (r == 0 || r == 6) ) + || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) { + _modules[row + r][col + c] = true; + } else { + _modules[row + r][col + c] = false; + } + } + } + }; + + var getBestMaskPattern = function() { + + var minLostPoint = 0; + var pattern = 0; + + for (var i = 0; i < 8; i += 1) { + + makeImpl(true, i); + + var lostPoint = QRUtil.getLostPoint(_this); + + if (i == 0 || minLostPoint > lostPoint) { + minLostPoint = lostPoint; + pattern = i; + } + } + + return pattern; + }; + + var setupTimingPattern = function() { + + for (var r = 8; r < _moduleCount - 8; r += 1) { + if (_modules[r][6] != null) { + continue; + } + _modules[r][6] = (r % 2 == 0); + } + + for (var c = 8; c < _moduleCount - 8; c += 1) { + if (_modules[6][c] != null) { + continue; + } + _modules[6][c] = (c % 2 == 0); + } + }; + + var setupPositionAdjustPattern = function() { + + var pos = QRUtil.getPatternPosition(_typeNumber); + + for (var i = 0; i < pos.length; i += 1) { + + for (var j = 0; j < pos.length; j += 1) { + + var row = pos[i]; + var col = pos[j]; + + if (_modules[row][col] != null) { + continue; + } + + for (var r = -2; r <= 2; r += 1) { + + for (var c = -2; c <= 2; c += 1) { + + if (r == -2 || r == 2 || c == -2 || c == 2 + || (r == 0 && c == 0) ) { + _modules[row + r][col + c] = true; + } else { + _modules[row + r][col + c] = false; + } + } + } + } + } + }; + + var setupTypeNumber = function(test) { + + var bits = QRUtil.getBCHTypeNumber(_typeNumber); + + for (var i = 0; i < 18; i += 1) { + var mod = (!test && ( (bits >> i) & 1) == 1); + _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod; + } + + for (var i = 0; i < 18; i += 1) { + var mod = (!test && ( (bits >> i) & 1) == 1); + _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod; + } + }; + + var setupTypeInfo = function(test, maskPattern) { + + var data = (_errorCorrectionLevel << 3) | maskPattern; + var bits = QRUtil.getBCHTypeInfo(data); + + // vertical + for (var i = 0; i < 15; i += 1) { + + var mod = (!test && ( (bits >> i) & 1) == 1); + + if (i < 6) { + _modules[i][8] = mod; + } else if (i < 8) { + _modules[i + 1][8] = mod; + } else { + _modules[_moduleCount - 15 + i][8] = mod; + } + } + + // horizontal + for (var i = 0; i < 15; i += 1) { + + var mod = (!test && ( (bits >> i) & 1) == 1); + + if (i < 8) { + _modules[8][_moduleCount - i - 1] = mod; + } else if (i < 9) { + _modules[8][15 - i - 1 + 1] = mod; + } else { + _modules[8][15 - i - 1] = mod; + } + } + + // fixed module + _modules[_moduleCount - 8][8] = (!test); + }; + + var mapData = function(data, maskPattern) { + + var inc = -1; + var row = _moduleCount - 1; + var bitIndex = 7; + var byteIndex = 0; + var maskFunc = QRUtil.getMaskFunction(maskPattern); + + for (var col = _moduleCount - 1; col > 0; col -= 2) { + + if (col == 6) col -= 1; + + while (true) { + + for (var c = 0; c < 2; c += 1) { + + if (_modules[row][col - c] == null) { + + var dark = false; + + if (byteIndex < data.length) { + dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1); + } + + var mask = maskFunc(row, col - c); + + if (mask) { + dark = !dark; + } + + _modules[row][col - c] = dark; + bitIndex -= 1; + + if (bitIndex == -1) { + byteIndex += 1; + bitIndex = 7; + } + } + } + + row += inc; + + if (row < 0 || _moduleCount <= row) { + row -= inc; + inc = -inc; + break; + } + } + } + }; + + var createBytes = function(buffer, rsBlocks) { + + var offset = 0; + + var maxDcCount = 0; + var maxEcCount = 0; + + var dcdata = new Array(rsBlocks.length); + var ecdata = new Array(rsBlocks.length); + + for (var r = 0; r < rsBlocks.length; r += 1) { + + var dcCount = rsBlocks[r].dataCount; + var ecCount = rsBlocks[r].totalCount - dcCount; + + maxDcCount = Math.max(maxDcCount, dcCount); + maxEcCount = Math.max(maxEcCount, ecCount); + + dcdata[r] = new Array(dcCount); + + for (var i = 0; i < dcdata[r].length; i += 1) { + dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset]; + } + offset += dcCount; + + var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); + var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1); + + var modPoly = rawPoly.mod(rsPoly); + ecdata[r] = new Array(rsPoly.getLength() - 1); + for (var i = 0; i < ecdata[r].length; i += 1) { + var modIndex = i + modPoly.getLength() - ecdata[r].length; + ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0; + } + } + + var totalCodeCount = 0; + for (var i = 0; i < rsBlocks.length; i += 1) { + totalCodeCount += rsBlocks[i].totalCount; + } + + var data = new Array(totalCodeCount); + var index = 0; + + for (var i = 0; i < maxDcCount; i += 1) { + for (var r = 0; r < rsBlocks.length; r += 1) { + if (i < dcdata[r].length) { + data[index] = dcdata[r][i]; + index += 1; + } + } + } + + for (var i = 0; i < maxEcCount; i += 1) { + for (var r = 0; r < rsBlocks.length; r += 1) { + if (i < ecdata[r].length) { + data[index] = ecdata[r][i]; + index += 1; + } + } + } + + return data; + }; + + var createData = function(typeNumber, errorCorrectionLevel, dataList) { + + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel); + + var buffer = qrBitBuffer(); + + for (var i = 0; i < dataList.length; i += 1) { + var data = dataList[i]; + buffer.put(data.getMode(), 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) ); + data.write(buffer); + } + + // calc num max data. + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i += 1) { + totalDataCount += rsBlocks[i].dataCount; + } + + if (buffer.getLengthInBits() > totalDataCount * 8) { + throw 'code length overflow. (' + + buffer.getLengthInBits() + + '>' + + totalDataCount * 8 + + ')'; + } + + // end code + if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { + buffer.put(0, 4); + } + + // padding + while (buffer.getLengthInBits() % 8 != 0) { + buffer.putBit(false); + } + + // padding + while (true) { + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(PAD0, 8); + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(PAD1, 8); + } + + return createBytes(buffer, rsBlocks); + }; + + _this.addData = function(data, mode) { + + mode = mode || 'Byte'; + + var newData = null; + + switch(mode) { + case 'Numeric' : + newData = qrNumber(data); + break; + case 'Alphanumeric' : + newData = qrAlphaNum(data); + break; + case 'Byte' : + newData = qr8BitByte(data); + break; + case 'Kanji' : + newData = qrKanji(data); + break; + default : + throw 'mode:' + mode; + } + + _dataList.push(newData); + _dataCache = null; + }; + + _this.isDark = function(row, col) { + if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) { + throw row + ',' + col; + } + return _modules[row][col]; + }; + + _this.getModuleCount = function() { + return _moduleCount; + }; + + _this.make = function() { + if (_typeNumber < 1) { + var typeNumber = 1; + + for (; typeNumber < 40; typeNumber++) { + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, _errorCorrectionLevel); + var buffer = qrBitBuffer(); + + for (var i = 0; i < _dataList.length; i++) { + var data = _dataList[i]; + buffer.put(data.getMode(), 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) ); + data.write(buffer); + } + + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount; + } + + if (buffer.getLengthInBits() <= totalDataCount * 8) { + break; + } + } + + _typeNumber = typeNumber; + } + + makeImpl(false, getBestMaskPattern() ); + }; + + _this.createTableTag = function(cellSize, margin) { + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + var qrHtml = ''; + + qrHtml += ''; + qrHtml += ''; + + for (var r = 0; r < _this.getModuleCount(); r += 1) { + + qrHtml += ''; + + for (var c = 0; c < _this.getModuleCount(); c += 1) { + qrHtml += ''; + } + + qrHtml += ''; + qrHtml += '
'; + } + + qrHtml += '
'; + + return qrHtml; + }; + + _this.createSvgTag = function(cellSize, margin, alt, title) { + + var opts = {}; + if (typeof arguments[0] == 'object') { + // Called by options. + opts = arguments[0]; + // overwrite cellSize and margin. + cellSize = opts.cellSize; + margin = opts.margin; + alt = opts.alt; + title = opts.title; + } + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + // Compose alt property surrogate + alt = (typeof alt === 'string') ? {text: alt} : alt || {}; + alt.text = alt.text || null; + alt.id = (alt.text) ? alt.id || 'qrcode-description' : null; + + // Compose title property surrogate + title = (typeof title === 'string') ? {text: title} : title || {}; + title.text = title.text || null; + title.id = (title.text) ? title.id || 'qrcode-title' : null; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var c, mc, r, mr, qrSvg='', rect; + + rect = 'l' + cellSize + ',0 0,' + cellSize + + ' -' + cellSize + ',0 0,-' + cellSize + 'z '; + + qrSvg += '' + + escapeXml(title.text) + '' : ''; + qrSvg += (alt.text) ? '' + + escapeXml(alt.text) + '' : ''; + qrSvg += ''; + qrSvg += ''; + qrSvg += ''; + + return qrSvg; + }; + + _this.createDataURL = function(cellSize, margin) { + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var min = margin; + var max = size - margin; + + return createDataURL(size, size, function(x, y) { + if (min <= x && x < max && min <= y && y < max) { + var c = Math.floor( (x - min) / cellSize); + var r = Math.floor( (y - min) / cellSize); + return _this.isDark(r, c)? 0 : 1; + } else { + return 1; + } + } ); + }; + + _this.createImgTag = function(cellSize, margin, alt) { + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + + var img = ''; + img += '': escaped += '>'; break; + case '&': escaped += '&'; break; + case '"': escaped += '"'; break; + default : escaped += c; break; + } + } + return escaped; + }; + + var _createHalfASCII = function(margin) { + var cellSize = 1; + margin = (typeof margin == 'undefined')? cellSize * 2 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var min = margin; + var max = size - margin; + + var y, x, r1, r2, p; + + var blocks = { + '██': '█', + '█ ': '▀', + ' █': '▄', + ' ': ' ' + }; + + var blocksLastLineNoMargin = { + '██': '▀', + '█ ': '▀', + ' █': ' ', + ' ': ' ' + }; + + var ascii = ''; + for (y = 0; y < size; y += 2) { + r1 = Math.floor((y - min) / cellSize); + r2 = Math.floor((y + 1 - min) / cellSize); + for (x = 0; x < size; x += 1) { + p = '█'; + + if (min <= x && x < max && min <= y && y < max && _this.isDark(r1, Math.floor((x - min) / cellSize))) { + p = ' '; + } + + if (min <= x && x < max && min <= y+1 && y+1 < max && _this.isDark(r2, Math.floor((x - min) / cellSize))) { + p += ' '; + } + else { + p += '█'; + } + + // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square. + ascii += (margin < 1 && y+1 >= max) ? blocksLastLineNoMargin[p] : blocks[p]; + } + + ascii += '\n'; + } + + if (size % 2 && margin > 0) { + return ascii.substring(0, ascii.length - size - 1) + Array(size+1).join('▀'); + } + + return ascii.substring(0, ascii.length-1); + }; + + _this.createASCII = function(cellSize, margin) { + cellSize = cellSize || 1; + + if (cellSize < 2) { + return _createHalfASCII(margin); + } + + cellSize -= 1; + margin = (typeof margin == 'undefined')? cellSize * 2 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var min = margin; + var max = size - margin; + + var y, x, r, p; + + var white = Array(cellSize+1).join('██'); + var black = Array(cellSize+1).join(' '); + + var ascii = ''; + var line = ''; + for (y = 0; y < size; y += 1) { + r = Math.floor( (y - min) / cellSize); + line = ''; + for (x = 0; x < size; x += 1) { + p = 1; + + if (min <= x && x < max && min <= y && y < max && _this.isDark(r, Math.floor((x - min) / cellSize))) { + p = 0; + } + + // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square. + line += p ? white : black; + } + + for (r = 0; r < cellSize; r += 1) { + ascii += line + '\n'; + } + } + + return ascii.substring(0, ascii.length-1); + }; + + _this.renderTo2dContext = function(context, cellSize) { + cellSize = cellSize || 2; + var length = _this.getModuleCount(); + for (var row = 0; row < length; row++) { + for (var col = 0; col < length; col++) { + context.fillStyle = _this.isDark(row, col) ? 'black' : 'white'; + context.fillRect(row * cellSize, col * cellSize, cellSize, cellSize); + } + } + } + + return _this; + }; + + //--------------------------------------------------------------------- + // qrcode.stringToBytes + //--------------------------------------------------------------------- + + qrcode.stringToBytesFuncs = { + 'default' : function(s) { + var bytes = []; + for (var i = 0; i < s.length; i += 1) { + var c = s.charCodeAt(i); + bytes.push(c & 0xff); + } + return bytes; + } + }; + + qrcode.stringToBytes = qrcode.stringToBytesFuncs['default']; + + //--------------------------------------------------------------------- + // qrcode.createStringToBytes + //--------------------------------------------------------------------- + + /** + * @param unicodeData base64 string of byte array. + * [16bit Unicode],[16bit Bytes], ... + * @param numChars + */ + qrcode.createStringToBytes = function(unicodeData, numChars) { + + // create conversion map. + + var unicodeMap = function() { + + var bin = base64DecodeInputStream(unicodeData); + var read = function() { + var b = bin.read(); + if (b == -1) throw 'eof'; + return b; + }; + + var count = 0; + var unicodeMap = {}; + while (true) { + var b0 = bin.read(); + if (b0 == -1) break; + var b1 = read(); + var b2 = read(); + var b3 = read(); + var k = String.fromCharCode( (b0 << 8) | b1); + var v = (b2 << 8) | b3; + unicodeMap[k] = v; + count += 1; + } + if (count != numChars) { + throw count + ' != ' + numChars; + } + + return unicodeMap; + }(); + + var unknownChar = '?'.charCodeAt(0); + + return function(s) { + var bytes = []; + for (var i = 0; i < s.length; i += 1) { + var c = s.charCodeAt(i); + if (c < 128) { + bytes.push(c); + } else { + var b = unicodeMap[s.charAt(i)]; + if (typeof b == 'number') { + if ( (b & 0xff) == b) { + // 1byte + bytes.push(b); + } else { + // 2bytes + bytes.push(b >>> 8); + bytes.push(b & 0xff); + } + } else { + bytes.push(unknownChar); + } + } + } + return bytes; + }; + }; + + //--------------------------------------------------------------------- + // QRMode + //--------------------------------------------------------------------- + + var QRMode = { + MODE_NUMBER : 1 << 0, + MODE_ALPHA_NUM : 1 << 1, + MODE_8BIT_BYTE : 1 << 2, + MODE_KANJI : 1 << 3 + }; + + //--------------------------------------------------------------------- + // QRErrorCorrectionLevel + //--------------------------------------------------------------------- + + var QRErrorCorrectionLevel = { + L : 1, + M : 0, + Q : 3, + H : 2 + }; + + //--------------------------------------------------------------------- + // QRMaskPattern + //--------------------------------------------------------------------- + + var QRMaskPattern = { + PATTERN000 : 0, + PATTERN001 : 1, + PATTERN010 : 2, + PATTERN011 : 3, + PATTERN100 : 4, + PATTERN101 : 5, + PATTERN110 : 6, + PATTERN111 : 7 + }; + + //--------------------------------------------------------------------- + // QRUtil + //--------------------------------------------------------------------- + + var QRUtil = function() { + + var PATTERN_POSITION_TABLE = [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ]; + var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0); + var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0); + var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1); + + var _this = {}; + + var getBCHDigit = function(data) { + var digit = 0; + while (data != 0) { + digit += 1; + data >>>= 1; + } + return digit; + }; + + _this.getBCHTypeInfo = function(data) { + var d = data << 10; + while (getBCHDigit(d) - getBCHDigit(G15) >= 0) { + d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) ); + } + return ( (data << 10) | d) ^ G15_MASK; + }; + + _this.getBCHTypeNumber = function(data) { + var d = data << 12; + while (getBCHDigit(d) - getBCHDigit(G18) >= 0) { + d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) ); + } + return (data << 12) | d; + }; + + _this.getPatternPosition = function(typeNumber) { + return PATTERN_POSITION_TABLE[typeNumber - 1]; + }; + + _this.getMaskFunction = function(maskPattern) { + + switch (maskPattern) { + + case QRMaskPattern.PATTERN000 : + return function(i, j) { return (i + j) % 2 == 0; }; + case QRMaskPattern.PATTERN001 : + return function(i, j) { return i % 2 == 0; }; + case QRMaskPattern.PATTERN010 : + return function(i, j) { return j % 3 == 0; }; + case QRMaskPattern.PATTERN011 : + return function(i, j) { return (i + j) % 3 == 0; }; + case QRMaskPattern.PATTERN100 : + return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; }; + case QRMaskPattern.PATTERN101 : + return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; }; + case QRMaskPattern.PATTERN110 : + return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; }; + case QRMaskPattern.PATTERN111 : + return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; }; + + default : + throw 'bad maskPattern:' + maskPattern; + } + }; + + _this.getErrorCorrectPolynomial = function(errorCorrectLength) { + var a = qrPolynomial([1], 0); + for (var i = 0; i < errorCorrectLength; i += 1) { + a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) ); + } + return a; + }; + + _this.getLengthInBits = function(mode, type) { + + if (1 <= type && type < 10) { + + // 1 - 9 + + switch(mode) { + case QRMode.MODE_NUMBER : return 10; + case QRMode.MODE_ALPHA_NUM : return 9; + case QRMode.MODE_8BIT_BYTE : return 8; + case QRMode.MODE_KANJI : return 8; + default : + throw 'mode:' + mode; + } + + } else if (type < 27) { + + // 10 - 26 + + switch(mode) { + case QRMode.MODE_NUMBER : return 12; + case QRMode.MODE_ALPHA_NUM : return 11; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 10; + default : + throw 'mode:' + mode; + } + + } else if (type < 41) { + + // 27 - 40 + + switch(mode) { + case QRMode.MODE_NUMBER : return 14; + case QRMode.MODE_ALPHA_NUM : return 13; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 12; + default : + throw 'mode:' + mode; + } + + } else { + throw 'type:' + type; + } + }; + + _this.getLostPoint = function(qrcode) { + + var moduleCount = qrcode.getModuleCount(); + + var lostPoint = 0; + + // LEVEL1 + + for (var row = 0; row < moduleCount; row += 1) { + for (var col = 0; col < moduleCount; col += 1) { + + var sameCount = 0; + var dark = qrcode.isDark(row, col); + + for (var r = -1; r <= 1; r += 1) { + + if (row + r < 0 || moduleCount <= row + r) { + continue; + } + + for (var c = -1; c <= 1; c += 1) { + + if (col + c < 0 || moduleCount <= col + c) { + continue; + } + + if (r == 0 && c == 0) { + continue; + } + + if (dark == qrcode.isDark(row + r, col + c) ) { + sameCount += 1; + } + } + } + + if (sameCount > 5) { + lostPoint += (3 + sameCount - 5); + } + } + }; + + // LEVEL2 + + for (var row = 0; row < moduleCount - 1; row += 1) { + for (var col = 0; col < moduleCount - 1; col += 1) { + var count = 0; + if (qrcode.isDark(row, col) ) count += 1; + if (qrcode.isDark(row + 1, col) ) count += 1; + if (qrcode.isDark(row, col + 1) ) count += 1; + if (qrcode.isDark(row + 1, col + 1) ) count += 1; + if (count == 0 || count == 4) { + lostPoint += 3; + } + } + } + + // LEVEL3 + + for (var row = 0; row < moduleCount; row += 1) { + for (var col = 0; col < moduleCount - 6; col += 1) { + if (qrcode.isDark(row, col) + && !qrcode.isDark(row, col + 1) + && qrcode.isDark(row, col + 2) + && qrcode.isDark(row, col + 3) + && qrcode.isDark(row, col + 4) + && !qrcode.isDark(row, col + 5) + && qrcode.isDark(row, col + 6) ) { + lostPoint += 40; + } + } + } + + for (var col = 0; col < moduleCount; col += 1) { + for (var row = 0; row < moduleCount - 6; row += 1) { + if (qrcode.isDark(row, col) + && !qrcode.isDark(row + 1, col) + && qrcode.isDark(row + 2, col) + && qrcode.isDark(row + 3, col) + && qrcode.isDark(row + 4, col) + && !qrcode.isDark(row + 5, col) + && qrcode.isDark(row + 6, col) ) { + lostPoint += 40; + } + } + } + + // LEVEL4 + + var darkCount = 0; + + for (var col = 0; col < moduleCount; col += 1) { + for (var row = 0; row < moduleCount; row += 1) { + if (qrcode.isDark(row, col) ) { + darkCount += 1; + } + } + } + + var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; + lostPoint += ratio * 10; + + return lostPoint; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // QRMath + //--------------------------------------------------------------------- + + var QRMath = function() { + + var EXP_TABLE = new Array(256); + var LOG_TABLE = new Array(256); + + // initialize tables + for (var i = 0; i < 8; i += 1) { + EXP_TABLE[i] = 1 << i; + } + for (var i = 8; i < 256; i += 1) { + EXP_TABLE[i] = EXP_TABLE[i - 4] + ^ EXP_TABLE[i - 5] + ^ EXP_TABLE[i - 6] + ^ EXP_TABLE[i - 8]; + } + for (var i = 0; i < 255; i += 1) { + LOG_TABLE[EXP_TABLE[i] ] = i; + } + + var _this = {}; + + _this.glog = function(n) { + + if (n < 1) { + throw 'glog(' + n + ')'; + } + + return LOG_TABLE[n]; + }; + + _this.gexp = function(n) { + + while (n < 0) { + n += 255; + } + + while (n >= 256) { + n -= 255; + } + + return EXP_TABLE[n]; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // qrPolynomial + //--------------------------------------------------------------------- + + function qrPolynomial(num, shift) { + + if (typeof num.length == 'undefined') { + throw num.length + '/' + shift; + } + + var _num = function() { + var offset = 0; + while (offset < num.length && num[offset] == 0) { + offset += 1; + } + var _num = new Array(num.length - offset + shift); + for (var i = 0; i < num.length - offset; i += 1) { + _num[i] = num[i + offset]; + } + return _num; + }(); + + var _this = {}; + + _this.getAt = function(index) { + return _num[index]; + }; + + _this.getLength = function() { + return _num.length; + }; + + _this.multiply = function(e) { + + var num = new Array(_this.getLength() + e.getLength() - 1); + + for (var i = 0; i < _this.getLength(); i += 1) { + for (var j = 0; j < e.getLength(); j += 1) { + num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) ); + } + } + + return qrPolynomial(num, 0); + }; + + _this.mod = function(e) { + + if (_this.getLength() - e.getLength() < 0) { + return _this; + } + + var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) ); + + var num = new Array(_this.getLength() ); + for (var i = 0; i < _this.getLength(); i += 1) { + num[i] = _this.getAt(i); + } + + for (var i = 0; i < e.getLength(); i += 1) { + num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio); + } + + // recursive call + return qrPolynomial(num, 0).mod(e); + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // QRRSBlock + //--------------------------------------------------------------------- + + var QRRSBlock = function() { + + var RS_BLOCK_TABLE = [ + + // L + // M + // Q + // H + + // 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + + // 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + + // 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + + // 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + + // 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + + // 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + + // 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + + // 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + + // 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + + // 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + + // 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + + // 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + + // 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + + // 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + + // 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12, 7, 37, 13], + + // 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + + // 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + + // 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + + // 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + + // 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + + // 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + + // 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + + // 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + + // 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + + // 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + + // 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + + // 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + + // 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + + // 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + + // 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + + // 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + + // 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + + // 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + + // 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + + // 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + + // 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + + // 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + + // 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + + // 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + + // 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] + ]; + + var qrRSBlock = function(totalCount, dataCount) { + var _this = {}; + _this.totalCount = totalCount; + _this.dataCount = dataCount; + return _this; + }; + + var _this = {}; + + var getRsBlockTable = function(typeNumber, errorCorrectionLevel) { + + switch(errorCorrectionLevel) { + case QRErrorCorrectionLevel.L : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; + case QRErrorCorrectionLevel.M : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; + case QRErrorCorrectionLevel.Q : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; + case QRErrorCorrectionLevel.H : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; + default : + return undefined; + } + }; + + _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) { + + var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel); + + if (typeof rsBlock == 'undefined') { + throw 'bad rs block @ typeNumber:' + typeNumber + + '/errorCorrectionLevel:' + errorCorrectionLevel; + } + + var length = rsBlock.length / 3; + + var list = []; + + for (var i = 0; i < length; i += 1) { + + var count = rsBlock[i * 3 + 0]; + var totalCount = rsBlock[i * 3 + 1]; + var dataCount = rsBlock[i * 3 + 2]; + + for (var j = 0; j < count; j += 1) { + list.push(qrRSBlock(totalCount, dataCount) ); + } + } + + return list; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // qrBitBuffer + //--------------------------------------------------------------------- + + var qrBitBuffer = function() { + + var _buffer = []; + var _length = 0; + + var _this = {}; + + _this.getBuffer = function() { + return _buffer; + }; + + _this.getAt = function(index) { + var bufIndex = Math.floor(index / 8); + return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1; + }; + + _this.put = function(num, length) { + for (var i = 0; i < length; i += 1) { + _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1); + } + }; + + _this.getLengthInBits = function() { + return _length; + }; + + _this.putBit = function(bit) { + + var bufIndex = Math.floor(_length / 8); + if (_buffer.length <= bufIndex) { + _buffer.push(0); + } + + if (bit) { + _buffer[bufIndex] |= (0x80 >>> (_length % 8) ); + } + + _length += 1; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qrNumber + //--------------------------------------------------------------------- + + var qrNumber = function(data) { + + var _mode = QRMode.MODE_NUMBER; + var _data = data; + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return _data.length; + }; + + _this.write = function(buffer) { + + var data = _data; + + var i = 0; + + while (i + 2 < data.length) { + buffer.put(strToNum(data.substring(i, i + 3) ), 10); + i += 3; + } + + if (i < data.length) { + if (data.length - i == 1) { + buffer.put(strToNum(data.substring(i, i + 1) ), 4); + } else if (data.length - i == 2) { + buffer.put(strToNum(data.substring(i, i + 2) ), 7); + } + } + }; + + var strToNum = function(s) { + var num = 0; + for (var i = 0; i < s.length; i += 1) { + num = num * 10 + chatToNum(s.charAt(i) ); + } + return num; + }; + + var chatToNum = function(c) { + if ('0' <= c && c <= '9') { + return c.charCodeAt(0) - '0'.charCodeAt(0); + } + throw 'illegal char :' + c; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qrAlphaNum + //--------------------------------------------------------------------- + + var qrAlphaNum = function(data) { + + var _mode = QRMode.MODE_ALPHA_NUM; + var _data = data; + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return _data.length; + }; + + _this.write = function(buffer) { + + var s = _data; + + var i = 0; + + while (i + 1 < s.length) { + buffer.put( + getCode(s.charAt(i) ) * 45 + + getCode(s.charAt(i + 1) ), 11); + i += 2; + } + + if (i < s.length) { + buffer.put(getCode(s.charAt(i) ), 6); + } + }; + + var getCode = function(c) { + + if ('0' <= c && c <= '9') { + return c.charCodeAt(0) - '0'.charCodeAt(0); + } else if ('A' <= c && c <= 'Z') { + return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10; + } else { + switch (c) { + case ' ' : return 36; + case '$' : return 37; + case '%' : return 38; + case '*' : return 39; + case '+' : return 40; + case '-' : return 41; + case '.' : return 42; + case '/' : return 43; + case ':' : return 44; + default : + throw 'illegal char :' + c; + } + } + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qr8BitByte + //--------------------------------------------------------------------- + + var qr8BitByte = function(data) { + + var _mode = QRMode.MODE_8BIT_BYTE; + var _data = data; + var _bytes = qrcode.stringToBytes(data); + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return _bytes.length; + }; + + _this.write = function(buffer) { + for (var i = 0; i < _bytes.length; i += 1) { + buffer.put(_bytes[i], 8); + } + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qrKanji + //--------------------------------------------------------------------- + + var qrKanji = function(data) { + + var _mode = QRMode.MODE_KANJI; + var _data = data; + + var stringToBytes = qrcode.stringToBytesFuncs['SJIS']; + if (!stringToBytes) { + throw 'sjis not supported.'; + } + !function(c, code) { + // self test for sjis support. + var test = stringToBytes(c); + if (test.length != 2 || ( (test[0] << 8) | test[1]) != code) { + throw 'sjis not supported.'; + } + }('\u53cb', 0x9746); + + var _bytes = stringToBytes(data); + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return ~~(_bytes.length / 2); + }; + + _this.write = function(buffer) { + + var data = _bytes; + + var i = 0; + + while (i + 1 < data.length) { + + var c = ( (0xff & data[i]) << 8) | (0xff & data[i + 1]); + + if (0x8140 <= c && c <= 0x9FFC) { + c -= 0x8140; + } else if (0xE040 <= c && c <= 0xEBBF) { + c -= 0xC140; + } else { + throw 'illegal char at ' + (i + 1) + '/' + c; + } + + c = ( (c >>> 8) & 0xff) * 0xC0 + (c & 0xff); + + buffer.put(c, 13); + + i += 2; + } + + if (i < data.length) { + throw 'illegal char at ' + (i + 1); + } + }; + + return _this; + }; + + //===================================================================== + // GIF Support etc. + // + + //--------------------------------------------------------------------- + // byteArrayOutputStream + //--------------------------------------------------------------------- + + var byteArrayOutputStream = function() { + + var _bytes = []; + + var _this = {}; + + _this.writeByte = function(b) { + _bytes.push(b & 0xff); + }; + + _this.writeShort = function(i) { + _this.writeByte(i); + _this.writeByte(i >>> 8); + }; + + _this.writeBytes = function(b, off, len) { + off = off || 0; + len = len || b.length; + for (var i = 0; i < len; i += 1) { + _this.writeByte(b[i + off]); + } + }; + + _this.writeString = function(s) { + for (var i = 0; i < s.length; i += 1) { + _this.writeByte(s.charCodeAt(i) ); + } + }; + + _this.toByteArray = function() { + return _bytes; + }; + + _this.toString = function() { + var s = ''; + s += '['; + for (var i = 0; i < _bytes.length; i += 1) { + if (i > 0) { + s += ','; + } + s += _bytes[i]; + } + s += ']'; + return s; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // base64EncodeOutputStream + //--------------------------------------------------------------------- + + var base64EncodeOutputStream = function() { + + var _buffer = 0; + var _buflen = 0; + var _length = 0; + var _base64 = ''; + + var _this = {}; + + var writeEncoded = function(b) { + _base64 += String.fromCharCode(encode(b & 0x3f) ); + }; + + var encode = function(n) { + if (n < 0) { + // error. + } else if (n < 26) { + return 0x41 + n; + } else if (n < 52) { + return 0x61 + (n - 26); + } else if (n < 62) { + return 0x30 + (n - 52); + } else if (n == 62) { + return 0x2b; + } else if (n == 63) { + return 0x2f; + } + throw 'n:' + n; + }; + + _this.writeByte = function(n) { + + _buffer = (_buffer << 8) | (n & 0xff); + _buflen += 8; + _length += 1; + + while (_buflen >= 6) { + writeEncoded(_buffer >>> (_buflen - 6) ); + _buflen -= 6; + } + }; + + _this.flush = function() { + + if (_buflen > 0) { + writeEncoded(_buffer << (6 - _buflen) ); + _buffer = 0; + _buflen = 0; + } + + if (_length % 3 != 0) { + // padding + var padlen = 3 - _length % 3; + for (var i = 0; i < padlen; i += 1) { + _base64 += '='; + } + } + }; + + _this.toString = function() { + return _base64; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // base64DecodeInputStream + //--------------------------------------------------------------------- + + var base64DecodeInputStream = function(str) { + + var _str = str; + var _pos = 0; + var _buffer = 0; + var _buflen = 0; + + var _this = {}; + + _this.read = function() { + + while (_buflen < 8) { + + if (_pos >= _str.length) { + if (_buflen == 0) { + return -1; + } + throw 'unexpected end of file./' + _buflen; + } + + var c = _str.charAt(_pos); + _pos += 1; + + if (c == '=') { + _buflen = 0; + return -1; + } else if (c.match(/^\s$/) ) { + // ignore if whitespace. + continue; + } + + _buffer = (_buffer << 6) | decode(c.charCodeAt(0) ); + _buflen += 6; + } + + var n = (_buffer >>> (_buflen - 8) ) & 0xff; + _buflen -= 8; + return n; + }; + + var decode = function(c) { + if (0x41 <= c && c <= 0x5a) { + return c - 0x41; + } else if (0x61 <= c && c <= 0x7a) { + return c - 0x61 + 26; + } else if (0x30 <= c && c <= 0x39) { + return c - 0x30 + 52; + } else if (c == 0x2b) { + return 62; + } else if (c == 0x2f) { + return 63; + } else { + throw 'c:' + c; + } + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // gifImage (B/W) + //--------------------------------------------------------------------- + + var gifImage = function(width, height) { + + var _width = width; + var _height = height; + var _data = new Array(width * height); + + var _this = {}; + + _this.setPixel = function(x, y, pixel) { + _data[y * _width + x] = pixel; + }; + + _this.write = function(out) { + + //--------------------------------- + // GIF Signature + + out.writeString('GIF87a'); + + //--------------------------------- + // Screen Descriptor + + out.writeShort(_width); + out.writeShort(_height); + + out.writeByte(0x80); // 2bit + out.writeByte(0); + out.writeByte(0); + + //--------------------------------- + // Global Color Map + + // black + out.writeByte(0x00); + out.writeByte(0x00); + out.writeByte(0x00); + + // white + out.writeByte(0xff); + out.writeByte(0xff); + out.writeByte(0xff); + + //--------------------------------- + // Image Descriptor + + out.writeString(','); + out.writeShort(0); + out.writeShort(0); + out.writeShort(_width); + out.writeShort(_height); + out.writeByte(0); + + //--------------------------------- + // Local Color Map + + //--------------------------------- + // Raster Data + + var lzwMinCodeSize = 2; + var raster = getLZWRaster(lzwMinCodeSize); + + out.writeByte(lzwMinCodeSize); + + var offset = 0; + + while (raster.length - offset > 255) { + out.writeByte(255); + out.writeBytes(raster, offset, 255); + offset += 255; + } + + out.writeByte(raster.length - offset); + out.writeBytes(raster, offset, raster.length - offset); + out.writeByte(0x00); + + //--------------------------------- + // GIF Terminator + out.writeString(';'); + }; + + var bitOutputStream = function(out) { + + var _out = out; + var _bitLength = 0; + var _bitBuffer = 0; + + var _this = {}; + + _this.write = function(data, length) { + + if ( (data >>> length) != 0) { + throw 'length over'; + } + + while (_bitLength + length >= 8) { + _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) ); + length -= (8 - _bitLength); + data >>>= (8 - _bitLength); + _bitBuffer = 0; + _bitLength = 0; + } + + _bitBuffer = (data << _bitLength) | _bitBuffer; + _bitLength = _bitLength + length; + }; + + _this.flush = function() { + if (_bitLength > 0) { + _out.writeByte(_bitBuffer); + } + }; + + return _this; + }; + + var getLZWRaster = function(lzwMinCodeSize) { + + var clearCode = 1 << lzwMinCodeSize; + var endCode = (1 << lzwMinCodeSize) + 1; + var bitLength = lzwMinCodeSize + 1; + + // Setup LZWTable + var table = lzwTable(); + + for (var i = 0; i < clearCode; i += 1) { + table.add(String.fromCharCode(i) ); + } + table.add(String.fromCharCode(clearCode) ); + table.add(String.fromCharCode(endCode) ); + + var byteOut = byteArrayOutputStream(); + var bitOut = bitOutputStream(byteOut); + + // clear code + bitOut.write(clearCode, bitLength); + + var dataIndex = 0; + + var s = String.fromCharCode(_data[dataIndex]); + dataIndex += 1; + + while (dataIndex < _data.length) { + + var c = String.fromCharCode(_data[dataIndex]); + dataIndex += 1; + + if (table.contains(s + c) ) { + + s = s + c; + + } else { + + bitOut.write(table.indexOf(s), bitLength); + + if (table.size() < 0xfff) { + + if (table.size() == (1 << bitLength) ) { + bitLength += 1; + } + + table.add(s + c); + } + + s = c; + } + } + + bitOut.write(table.indexOf(s), bitLength); + + // end code + bitOut.write(endCode, bitLength); + + bitOut.flush(); + + return byteOut.toByteArray(); + }; + + var lzwTable = function() { + + var _map = {}; + var _size = 0; + + var _this = {}; + + _this.add = function(key) { + if (_this.contains(key) ) { + throw 'dup key:' + key; + } + _map[key] = _size; + _size += 1; + }; + + _this.size = function() { + return _size; + }; + + _this.indexOf = function(key) { + return _map[key]; + }; + + _this.contains = function(key) { + return typeof _map[key] != 'undefined'; + }; + + return _this; + }; + + return _this; + }; + + var createDataURL = function(width, height, getPixel) { + var gif = gifImage(width, height); + for (var y = 0; y < height; y += 1) { + for (var x = 0; x < width; x += 1) { + gif.setPixel(x, y, getPixel(x, y) ); + } + } + + var b = byteArrayOutputStream(); + gif.write(b); + + var base64 = base64EncodeOutputStream(); + var bytes = b.toByteArray(); + for (var i = 0; i < bytes.length; i += 1) { + base64.writeByte(bytes[i]); + } + base64.flush(); + + return 'data:image/gif;base64,' + base64; + }; + + //--------------------------------------------------------------------- + // returns qrcode function. + + return qrcode; + }(); + + // multibyte support + !function() { + + qrcode.stringToBytesFuncs['UTF-8'] = function(s) { + // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array + function toUTF8Array(str) { + var utf8 = []; + for (var i=0; i < str.length; i++) { + var charcode = str.charCodeAt(i); + if (charcode < 0x80) utf8.push(charcode); + else if (charcode < 0x800) { + utf8.push(0xc0 | (charcode >> 6), + 0x80 | (charcode & 0x3f)); + } + else if (charcode < 0xd800 || charcode >= 0xe000) { + utf8.push(0xe0 | (charcode >> 12), + 0x80 | ((charcode>>6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + // surrogate pair + else { + i++; + // UTF-16 encodes 0x10000-0x10FFFF by + // subtracting 0x10000 and splitting the + // 20 bits of 0x0-0xFFFFF into two halves + charcode = 0x10000 + (((charcode & 0x3ff)<<10) + | (str.charCodeAt(i) & 0x3ff)); + utf8.push(0xf0 | (charcode >>18), + 0x80 | ((charcode>>12) & 0x3f), + 0x80 | ((charcode>>6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + } + return utf8; + } + return toUTF8Array(s); + }; + + }(); + + (function (factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } + }(function () { + return qrcode; + })); \ No newline at end of file