quicknode-sdk 0.8.0__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/Cargo.lock +5 -5
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/Cargo.toml +1 -1
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/PKG-INFO +5 -3
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/Cargo.toml +1 -1
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/README.md +4 -2
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/rpc_payment.rs +3 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/rpc/payment/drawdown.rs +167 -21
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/rpc/payment/signer/mod.rs +16 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/rpc/payment/signer/svm.rs +7 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/pyproject.toml +1 -1
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/python/README.md +4 -2
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/LICENSE +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/LICENSE +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/admin.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/admin_e2e.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/kvstore_e2e.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/rpc.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/sql.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/streams.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/streams_e2e.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/examples/webhooks_e2e.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/account.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/api_credits.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/billing.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/bulk.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/chains.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/endpoint_metrics.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/endpoint_rate_limits.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/endpoint_security.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/endpoint_urls.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/endpoints.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/logs.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/mod.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/tags.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/teams.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/tooling_access.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/admin/usage.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/config.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/errors.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/kvstore/mod.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/lib.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/rpc/mod.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/rpc/payment/mod.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/rpc/payment/session.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/rpc/payment/signer/tempo.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/sql/mod.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/streams/mod.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/streams/stream.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/webhooks/mod.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/core/src/webhooks/webhook.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/python/Cargo.toml +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/python/src/errors.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/python/src/lib.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/python/src/sql.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/python/src/streams_destination.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/crates/python/src/webhooks_template.rs +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/python/quicknode_sdk/__init__.py +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/python/quicknode_sdk/__init__.pyi +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/python/quicknode_sdk/_core/__init__.pyi +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/python/quicknode_sdk/init_manual_override.pyi +0 -0
- {quicknode_sdk-0.8.0 → quicknode_sdk-0.8.1}/python/quicknode_sdk/py.typed +0 -0
|
@@ -3327,7 +3327,7 @@ dependencies = [
|
|
|
3327
3327
|
|
|
3328
3328
|
[[package]]
|
|
3329
3329
|
name = "quicknode-sdk"
|
|
3330
|
-
version = "0.8.
|
|
3330
|
+
version = "0.8.1"
|
|
3331
3331
|
dependencies = [
|
|
3332
3332
|
"alloy-consensus",
|
|
3333
3333
|
"alloy-primitives",
|
|
@@ -3999,7 +3999,7 @@ dependencies = [
|
|
|
3999
3999
|
|
|
4000
4000
|
[[package]]
|
|
4001
4001
|
name = "sdk-node"
|
|
4002
|
-
version = "0.8.
|
|
4002
|
+
version = "0.8.1"
|
|
4003
4003
|
dependencies = [
|
|
4004
4004
|
"napi",
|
|
4005
4005
|
"napi-build",
|
|
@@ -4011,7 +4011,7 @@ dependencies = [
|
|
|
4011
4011
|
|
|
4012
4012
|
[[package]]
|
|
4013
4013
|
name = "sdk-python"
|
|
4014
|
-
version = "0.8.
|
|
4014
|
+
version = "0.8.1"
|
|
4015
4015
|
dependencies = [
|
|
4016
4016
|
"pyo3",
|
|
4017
4017
|
"pyo3-async-runtimes",
|
|
@@ -4023,7 +4023,7 @@ dependencies = [
|
|
|
4023
4023
|
|
|
4024
4024
|
[[package]]
|
|
4025
4025
|
name = "sdk-python-stubs"
|
|
4026
|
-
version = "0.8.
|
|
4026
|
+
version = "0.8.1"
|
|
4027
4027
|
dependencies = [
|
|
4028
4028
|
"pyo3-stub-gen",
|
|
4029
4029
|
"sdk-python",
|
|
@@ -4031,7 +4031,7 @@ dependencies = [
|
|
|
4031
4031
|
|
|
4032
4032
|
[[package]]
|
|
4033
4033
|
name = "sdk-ruby"
|
|
4034
|
-
version = "0.8.
|
|
4034
|
+
version = "0.8.1"
|
|
4035
4035
|
dependencies = [
|
|
4036
4036
|
"magnus",
|
|
4037
4037
|
"quicknode-sdk",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quicknode-sdk
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Classifier: Intended Audience :: Developers
|
|
5
5
|
Classifier: License :: OSI Approved :: MIT License
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -1846,7 +1846,7 @@ open("payment.key", "w").write(wallet["key"]) # returned exactly once
|
|
|
1846
1846
|
|
|
1847
1847
|
### x402 credit drawdown (authenticate once, then draw one credit per call)
|
|
1848
1848
|
|
|
1849
|
-
Cheaper per call than paying per request: one SIWE signature mints a session JWT, then
|
|
1849
|
+
Cheaper per call than paying per request: one SIWE or SIWS signature mints a session JWT, then
|
|
1850
1850
|
each call draws a single credit from the account balance instead of signing a fresh
|
|
1851
1851
|
settlement. Minting the session is free and moves no funds, so a host can re-authenticate
|
|
1852
1852
|
transparently. Persist it between processes.
|
|
@@ -1854,7 +1854,9 @@ transparently. Persist it between processes.
|
|
|
1854
1854
|
Fund the payment wallet out of band — the testnet faucet below, or by sending funds to
|
|
1855
1855
|
`payment_address()` directly. Credits are provisioned against the account gateway-side.
|
|
1856
1856
|
|
|
1857
|
-
EVM
|
|
1857
|
+
EVM payment networks use SIWE. Solana payment networks use SIWS with an Ed25519
|
|
1858
|
+
signature encoded as Base58. Solana wallets must be funded out of band; the faucet
|
|
1859
|
+
is available for Base Sepolia only.
|
|
1858
1860
|
|
|
1859
1861
|
| Method | Cost | Returns |
|
|
1860
1862
|
|---|---|---|
|
|
@@ -1945,7 +1945,7 @@ std::fs::write("payment.key", wallet.into_key())?; // consuming: a deliberate,
|
|
|
1945
1945
|
|
|
1946
1946
|
### x402 credit drawdown (authenticate once, then draw one credit per call)
|
|
1947
1947
|
|
|
1948
|
-
Cheaper per call than paying per request: one SIWE signature mints a session JWT, then
|
|
1948
|
+
Cheaper per call than paying per request: one SIWE or SIWS signature mints a session JWT, then
|
|
1949
1949
|
each call draws a single credit from the account balance instead of signing a fresh
|
|
1950
1950
|
settlement. Minting the JWT is free and moves no funds, so a host can re-authenticate
|
|
1951
1951
|
transparently. Persist the session between processes.
|
|
@@ -1953,7 +1953,9 @@ transparently. Persist the session between processes.
|
|
|
1953
1953
|
Fund the payment wallet out of band — the testnet faucet below, or by sending funds to
|
|
1954
1954
|
`payment_address()` directly. Credits are provisioned against the account gateway-side.
|
|
1955
1955
|
|
|
1956
|
-
EVM
|
|
1956
|
+
EVM payment networks use SIWE. Solana payment networks use SIWS with an Ed25519
|
|
1957
|
+
signature encoded as Base58. Solana wallets must be funded out of band; the faucet
|
|
1958
|
+
is available for Base Sepolia only.
|
|
1957
1959
|
|
|
1958
1960
|
| Method | Cost | Returns |
|
|
1959
1961
|
|---|---|---|
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
//! QN_PAYMENT_KEY=0x<throwaway-key> \
|
|
10
10
|
//! cargo run --example rpc_payment -p quicknode-sdk \
|
|
11
11
|
//! --features rust,payments,payments-svm,payments-tempo
|
|
12
|
+
//!
|
|
13
|
+
//! For Solana drawdown, use a base58 Solana key, a `solana:<genesis-hash>`
|
|
14
|
+
//! pay network, and the Solana USDC mint; authentication uses SIWS.
|
|
12
15
|
|
|
13
16
|
use quicknode_sdk::{PaymentConfig, QuicknodeSdk, RpcConfig, SdkFullConfig};
|
|
14
17
|
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
//! response — no per-call signing.
|
|
8
8
|
//!
|
|
9
9
|
//! The flow:
|
|
10
|
-
//! 1. [`authenticate`] — build a SIWE
|
|
10
|
+
//! 1. [`authenticate`] — build a SIWE or SIWS message, sign it with the
|
|
11
11
|
//! payment key, POST `/auth`, and cache the returned [`GatewaySession`].
|
|
12
12
|
//! 2. [`drawdown_call`] — POST `/:network` with the Bearer JWT; returns the raw
|
|
13
13
|
//! JSON-RPC envelope text.
|
|
14
14
|
//! 3. [`credits`] — GET `/credits` with the Bearer JWT → the current balance.
|
|
15
|
-
//! 4. [`drip`] — POST `/drip` (
|
|
16
|
-
//! wallet, not the credit ledger.
|
|
15
|
+
//! 4. [`drip`] — POST `/drip` (Base Sepolia faucet, once per account) — funds
|
|
16
|
+
//! the wallet, not the credit ledger.
|
|
17
17
|
//!
|
|
18
18
|
//! [`buy_credits`] settles a credit block by signing the gateway's credit-tier
|
|
19
19
|
//! offer. It is reachable only where that offer's construction is signable; see
|
|
@@ -100,31 +100,52 @@ const SIWX_STATEMENT: &str =
|
|
|
100
100
|
/// returns a cached [`GatewaySession`]. Free — no funds move — so a caller may
|
|
101
101
|
/// (re)auth transparently on a missing/expired session without user consent.
|
|
102
102
|
///
|
|
103
|
-
///
|
|
104
|
-
/// construction.
|
|
103
|
+
/// Selects SIWE for EVM payment networks and SIWS for Solana payment networks.
|
|
105
104
|
pub async fn authenticate(
|
|
106
105
|
client: &reqwest::Client,
|
|
107
106
|
payment: &ResolvedPayment,
|
|
108
107
|
) -> Result<GatewaySession, SdkError> {
|
|
109
108
|
let base = super::PaymentScheme::X402.host_base(payment.base_url_override.as_deref());
|
|
110
|
-
|
|
111
|
-
let address = to_checksum_address(&payment.signer.address()?);
|
|
112
|
-
// SIWE requires the decimal EIP-155 id, not the CAIP-2 string.
|
|
113
|
-
let chain_id = eip155_chain_id(&payment.pay_network)?;
|
|
114
|
-
|
|
115
|
-
// Build the gateway's fixed SIWE message with a fresh nonce and timestamp.
|
|
109
|
+
let address = payment.signer.address()?;
|
|
116
110
|
let host = host_only(base);
|
|
117
111
|
let nonce = hex::encode(&random_nonce()[..8]);
|
|
118
112
|
let issued_at = rfc3339_now();
|
|
119
|
-
let message =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
113
|
+
let (message, signature) = match payment.signer.kind() {
|
|
114
|
+
super::signer::ChainKind::Svm => {
|
|
115
|
+
let chain_id = solana_chain_id(&payment.pay_network)?;
|
|
116
|
+
let message = siws_message(
|
|
117
|
+
&host,
|
|
118
|
+
&address,
|
|
119
|
+
chain_id,
|
|
120
|
+
&nonce,
|
|
121
|
+
&issued_at,
|
|
122
|
+
SIWX_STATEMENT,
|
|
123
|
+
);
|
|
124
|
+
let signature = payment.signer.sign_siws(&message)?;
|
|
125
|
+
(message, signature)
|
|
126
|
+
}
|
|
127
|
+
super::signer::ChainKind::Evm => {
|
|
128
|
+
// SIWE compares the recovered address case-sensitively and uses
|
|
129
|
+
// the decimal EIP-155 id rather than the CAIP-2 string.
|
|
130
|
+
let address = to_checksum_address(&address);
|
|
131
|
+
let chain_id = eip155_chain_id(&payment.pay_network)?;
|
|
132
|
+
let message = siwe_message(
|
|
133
|
+
&host,
|
|
134
|
+
&address,
|
|
135
|
+
chain_id,
|
|
136
|
+
&nonce,
|
|
137
|
+
&issued_at,
|
|
138
|
+
SIWX_STATEMENT,
|
|
139
|
+
);
|
|
140
|
+
let signature = payment.signer.sign_siwe(&message)?;
|
|
141
|
+
(message, signature)
|
|
142
|
+
}
|
|
143
|
+
super::signer::ChainKind::Tempo => {
|
|
144
|
+
return Err(SdkError::Config(
|
|
145
|
+
"x402 drawdown requires an EVM or Solana signer".into(),
|
|
146
|
+
));
|
|
147
|
+
}
|
|
148
|
+
};
|
|
128
149
|
|
|
129
150
|
let url = format!("{}/auth", base.trim_end_matches('/'));
|
|
130
151
|
let resp = client
|
|
@@ -224,7 +245,7 @@ pub struct DripReceipt {
|
|
|
224
245
|
|
|
225
246
|
/// Requests testnet tokens from the faucet (POST `/drip`, Bearer JWT). The
|
|
226
247
|
/// gateway allows this once per account on Base Sepolia and returns the funding
|
|
227
|
-
/// transaction (NOT a balance).
|
|
248
|
+
/// transaction (NOT a balance). Solana wallets must be funded out of band.
|
|
228
249
|
pub async fn drip(
|
|
229
250
|
client: &reqwest::Client,
|
|
230
251
|
payment: &ResolvedPayment,
|
|
@@ -367,6 +388,29 @@ pub(super) fn siwe_message(
|
|
|
367
388
|
)
|
|
368
389
|
}
|
|
369
390
|
|
|
391
|
+
/// Build the CAIP-122 Sign-In-With-Solana message expected by the gateway.
|
|
392
|
+
pub(super) fn siws_message(
|
|
393
|
+
host: &str,
|
|
394
|
+
address: &str,
|
|
395
|
+
chain_id: &str,
|
|
396
|
+
nonce: &str,
|
|
397
|
+
issued_at: &str,
|
|
398
|
+
statement: &str,
|
|
399
|
+
) -> String {
|
|
400
|
+
format!(
|
|
401
|
+
"{host} wants you to sign in with your Solana account:\n\
|
|
402
|
+
{address}\n\
|
|
403
|
+
\n\
|
|
404
|
+
{statement}\n\
|
|
405
|
+
\n\
|
|
406
|
+
URI: https://{host}\n\
|
|
407
|
+
Version: 1\n\
|
|
408
|
+
Chain ID: {chain_id}\n\
|
|
409
|
+
Nonce: {nonce}\n\
|
|
410
|
+
Issued At: {issued_at}"
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
370
414
|
// Apply the EIP-55 checksum required by SIWE.
|
|
371
415
|
fn to_checksum_address(addr: &str) -> String {
|
|
372
416
|
use sha3::{Digest, Keccak256};
|
|
@@ -403,6 +447,18 @@ fn eip155_chain_id(pay_network: &str) -> Result<u64, SdkError> {
|
|
|
403
447
|
SdkError::Config(format!(
|
|
404
448
|
"x402 drawdown requires an eip155 pay network (e.g. eip155:84532), got {pay_network:?}"
|
|
405
449
|
))
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// SIWS displays the Solana genesis hash without the CAIP-2 namespace prefix.
|
|
454
|
+
fn solana_chain_id(pay_network: &str) -> Result<&str, SdkError> {
|
|
455
|
+
pay_network
|
|
456
|
+
.strip_prefix("solana:")
|
|
457
|
+
.filter(|chain_id| !chain_id.is_empty())
|
|
458
|
+
.ok_or_else(|| {
|
|
459
|
+
SdkError::Config(format!(
|
|
460
|
+
"x402 Solana drawdown requires a solana pay network, got {pay_network:?}"
|
|
461
|
+
))
|
|
406
462
|
})
|
|
407
463
|
}
|
|
408
464
|
|
|
@@ -466,6 +522,28 @@ mod tests {
|
|
|
466
522
|
}
|
|
467
523
|
}
|
|
468
524
|
|
|
525
|
+
#[cfg(feature = "payments-svm")]
|
|
526
|
+
fn svm_payment(base: &str) -> ResolvedPayment {
|
|
527
|
+
use ed25519_dalek::SigningKey;
|
|
528
|
+
|
|
529
|
+
let seed = [7u8; 32];
|
|
530
|
+
let signing_key = SigningKey::from_bytes(&seed);
|
|
531
|
+
let mut secret = Vec::with_capacity(64);
|
|
532
|
+
secret.extend_from_slice(&seed);
|
|
533
|
+
secret.extend_from_slice(&signing_key.verifying_key().to_bytes());
|
|
534
|
+
ResolvedPayment {
|
|
535
|
+
scheme: super::super::PaymentScheme::X402,
|
|
536
|
+
signer: super::super::signer::Signer::Svm(SecretString::new(
|
|
537
|
+
bs58::encode(secret).into_string(),
|
|
538
|
+
)),
|
|
539
|
+
pay_network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1".into(),
|
|
540
|
+
asset: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".into(),
|
|
541
|
+
max_amount: 10_000_000,
|
|
542
|
+
base_url_override: Some(base.to_string()),
|
|
543
|
+
svm_rpc_url: None,
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
469
547
|
fn x402_credit_offer(amount: &str) -> Value {
|
|
470
548
|
json!({
|
|
471
549
|
"x402Version": 2,
|
|
@@ -504,6 +582,30 @@ mod tests {
|
|
|
504
582
|
assert_eq!(msg, expected);
|
|
505
583
|
}
|
|
506
584
|
|
|
585
|
+
#[cfg(feature = "payments-svm")]
|
|
586
|
+
#[test]
|
|
587
|
+
fn siws_message_is_byte_exact() {
|
|
588
|
+
let msg = siws_message(
|
|
589
|
+
"x402.quicknode.com",
|
|
590
|
+
"11111111111111111111111111111111",
|
|
591
|
+
"EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
592
|
+
"abc12345",
|
|
593
|
+
"2026-07-17T12:00:00Z",
|
|
594
|
+
SIWX_STATEMENT,
|
|
595
|
+
);
|
|
596
|
+
let expected = "x402.quicknode.com wants you to sign in with your Solana account:\n\
|
|
597
|
+
11111111111111111111111111111111\n\
|
|
598
|
+
\n\
|
|
599
|
+
I accept the Quicknode Terms of Service: https://www.quicknode.com/terms\n\
|
|
600
|
+
\n\
|
|
601
|
+
URI: https://x402.quicknode.com\n\
|
|
602
|
+
Version: 1\n\
|
|
603
|
+
Chain ID: EtWTRABZaYq6iMfeYKouRu166VU2xqa1\n\
|
|
604
|
+
Nonce: abc12345\n\
|
|
605
|
+
Issued At: 2026-07-17T12:00:00Z";
|
|
606
|
+
assert_eq!(msg, expected);
|
|
607
|
+
}
|
|
608
|
+
|
|
507
609
|
// Verify the generated timestamp uses the gateway's millisecond format.
|
|
508
610
|
#[test]
|
|
509
611
|
fn issued_at_carries_millisecond_precision() {
|
|
@@ -609,6 +711,50 @@ mod tests {
|
|
|
609
711
|
assert!(session.is_fresh(60));
|
|
610
712
|
}
|
|
611
713
|
|
|
714
|
+
#[cfg(feature = "payments-svm")]
|
|
715
|
+
#[tokio::test]
|
|
716
|
+
async fn authenticate_posts_solana_siwx_with_base58_signature() {
|
|
717
|
+
use ed25519_dalek::{Signature, SigningKey, Verifier};
|
|
718
|
+
|
|
719
|
+
struct AuthResponder;
|
|
720
|
+
impl Respond for AuthResponder {
|
|
721
|
+
fn respond(&self, req: &Request) -> ResponseTemplate {
|
|
722
|
+
let body: Value = serde_json::from_slice(&req.body).unwrap();
|
|
723
|
+
assert_eq!(body["type"], "siwx");
|
|
724
|
+
let message = body["message"].as_str().unwrap();
|
|
725
|
+
assert!(message.contains("sign in with your Solana account"));
|
|
726
|
+
assert!(message.contains("Chain ID: EtWTRABZaYq6iMfeYKouRu166VU2xqa1"));
|
|
727
|
+
|
|
728
|
+
let signature = bs58::decode(body["signature"].as_str().unwrap())
|
|
729
|
+
.into_vec()
|
|
730
|
+
.unwrap();
|
|
731
|
+
let signature: [u8; 64] = signature.try_into().unwrap();
|
|
732
|
+
let public_key = SigningKey::from_bytes(&[7u8; 32]).verifying_key();
|
|
733
|
+
public_key
|
|
734
|
+
.verify(message.as_bytes(), &Signature::from_bytes(&signature))
|
|
735
|
+
.unwrap();
|
|
736
|
+
|
|
737
|
+
ResponseTemplate::new(200).set_body_json(json!({
|
|
738
|
+
"token": "jwt-solana",
|
|
739
|
+
"expiresAt": "2099-01-01T00:00:00Z",
|
|
740
|
+
"accountId": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1:11111111111111111111111111111111"
|
|
741
|
+
}))
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let server = MockServer::start().await;
|
|
746
|
+
Mock::given(method("POST"))
|
|
747
|
+
.and(path("/auth"))
|
|
748
|
+
.respond_with(AuthResponder)
|
|
749
|
+
.mount(&server)
|
|
750
|
+
.await;
|
|
751
|
+
|
|
752
|
+
let payment = svm_payment(&server.uri());
|
|
753
|
+
let client = reqwest::Client::new();
|
|
754
|
+
let session = authenticate(&client, &payment).await.unwrap();
|
|
755
|
+
assert_eq!(session.token, "jwt-solana");
|
|
756
|
+
}
|
|
757
|
+
|
|
612
758
|
#[tokio::test]
|
|
613
759
|
async fn authenticate_error_surfaces_as_api() {
|
|
614
760
|
let server = MockServer::start().await;
|
|
@@ -205,6 +205,22 @@ impl Signer {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/// Sign a Sign-In-With-Solana (CAIP-122) message with Ed25519 and return
|
|
209
|
+
/// the Base58 signature. Solana only; EVM signers use [`Self::sign_siwe`].
|
|
210
|
+
pub fn sign_siws(&self, _message: &str) -> Result<String, SdkError> {
|
|
211
|
+
match self {
|
|
212
|
+
#[cfg(feature = "payments-svm")]
|
|
213
|
+
Signer::Svm(_) => svm::sign_siws(self, _message),
|
|
214
|
+
Signer::Evm(_) | Signer::Tempo(_) => Err(SdkError::Config(
|
|
215
|
+
"SIWS signing requires an SVM signer".into(),
|
|
216
|
+
)),
|
|
217
|
+
#[cfg(not(feature = "payments-svm"))]
|
|
218
|
+
Signer::Svm(_) => Err(SdkError::Config(
|
|
219
|
+
"SIWS signing requires the `payments-svm` feature".into(),
|
|
220
|
+
)),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
208
224
|
/// Sign an MPP session voucher (`Voucher(bytes32 channelId,uint128
|
|
209
225
|
/// cumulativeAmount)`) against the legacy escrow contract's EIP-712 domain
|
|
210
226
|
/// ("Tempo Stream Channel"), returning the `0x`-prefixed 65-byte `r||s||v`
|
|
@@ -194,6 +194,13 @@ impl Signer {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/// Sign a CAIP-122 SIWS message and return the Base58 Ed25519 signature.
|
|
198
|
+
pub(super) fn sign_siws(signer: &Signer, message: &str) -> Result<String, SdkError> {
|
|
199
|
+
let key = svm_signing_key(signer)?;
|
|
200
|
+
let signature = key.sign(message.as_bytes());
|
|
201
|
+
Ok(bs58::encode(signature.to_bytes()).into_string())
|
|
202
|
+
}
|
|
203
|
+
|
|
197
204
|
/// A v0 message's account-permission counts.
|
|
198
205
|
struct MessageHeader {
|
|
199
206
|
num_required_signatures: u8,
|
|
@@ -1822,7 +1822,7 @@ open("payment.key", "w").write(wallet["key"]) # returned exactly once
|
|
|
1822
1822
|
|
|
1823
1823
|
### x402 credit drawdown (authenticate once, then draw one credit per call)
|
|
1824
1824
|
|
|
1825
|
-
Cheaper per call than paying per request: one SIWE signature mints a session JWT, then
|
|
1825
|
+
Cheaper per call than paying per request: one SIWE or SIWS signature mints a session JWT, then
|
|
1826
1826
|
each call draws a single credit from the account balance instead of signing a fresh
|
|
1827
1827
|
settlement. Minting the session is free and moves no funds, so a host can re-authenticate
|
|
1828
1828
|
transparently. Persist it between processes.
|
|
@@ -1830,7 +1830,9 @@ transparently. Persist it between processes.
|
|
|
1830
1830
|
Fund the payment wallet out of band — the testnet faucet below, or by sending funds to
|
|
1831
1831
|
`payment_address()` directly. Credits are provisioned against the account gateway-side.
|
|
1832
1832
|
|
|
1833
|
-
EVM
|
|
1833
|
+
EVM payment networks use SIWE. Solana payment networks use SIWS with an Ed25519
|
|
1834
|
+
signature encoded as Base58. Solana wallets must be funded out of band; the faucet
|
|
1835
|
+
is available for Base Sepolia only.
|
|
1834
1836
|
|
|
1835
1837
|
| Method | Cost | Returns |
|
|
1836
1838
|
|---|---|---|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|