Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/payment-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@parity/truapi": major
---

`payment.request` takes a caller-chosen 32-byte `id` and answers with nothing, adding `AlreadyExists`; `payment.statusSubscribe` follows that `id` and reports `PartiallyClaimed`. Hosts serve both through `PaymentPlatform` (`set_payment_callbacks` and `notify_payment_status` natively); without one they answer `Unsupported`.
28 changes: 15 additions & 13 deletions docs/rfcs/0006-payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,23 @@ enum PaymentTopUpStatusErr {

#### 3. Request Payment

Request a payment from the user's available balance to a destination account. The host should prompt the user to authorize the payment. Returns a `PaymentId` for tracking.
Request a payment from the user's available balance to a destination account. The host should prompt the user to authorize the payment. The caller chooses the `PaymentId` the payment is followed by.

```rust
fn host_payment_request(
amount: Balance,
destination: AccountId
) -> Result<PaymentReceipt, PaymentRequestErr>

type PaymentId = str;

struct PaymentReceipt {
destination: AccountId,
id: PaymentId
}
) -> Result<(), PaymentRequestErr>

/// Caller-chosen, 32 bytes. Reusing one is refused, which makes a retry safe.
type PaymentId = [u8; 32];

enum PaymentRequestErr {
/// A payment with this id already exists
AlreadyExists,
/// User denied the payment request
Denied,
Rejected,
/// User's available balance is not sufficient for the requested amount
InsufficientBalance,
Unknown(GenericErr)
Expand All @@ -145,11 +145,11 @@ A successful response means the user has authorized the payment and the host has

#### 4. Payment Status Subscription

Subscribe to status updates for a previously requested payment. The subscription emits status changes until the payment reaches a terminal state (`Completed` or `Failed`).
Subscribe to status updates for a previously requested payment. The subscription emits status changes until the payment reaches a terminal state (`Completed`, `Failed` or `PartiallyClaimed`), which stays readable after the payment ends.

```rust
fn host_payment_status_subscribe(
payment_id: PaymentId,
id: PaymentId,
callback: fn(PaymentStatus)
) -> Result<Subscriber, PaymentStatusErr>

Expand All @@ -159,7 +159,9 @@ enum PaymentStatus {
/// Payment has been settled successfully
Completed,
/// Payment has failed
Failed(str)
Failed(str),
/// Only this amount reached the destination, less than requested
PartiallyClaimed(Balance)
}

enum PaymentStatusErr {
Expand All @@ -177,7 +179,7 @@ enum PaymentStatusErr {

3. **Payment ID scoping**: A `PaymentId` is scoped to the product that created it. A product cannot query or subscribe to payment status for another product's payments.

4. **Terminal status delivery**: Once a payment reaches `Completed` or `Failed`, the host must deliver that status to any active subscriber and may then close the subscription. The host should make a best effort to deliver terminal status even across session restarts.
4. **Terminal status delivery**: Once a payment reaches `Completed`, `Failed` or `PartiallyClaimed`, the host must deliver that status to any active subscriber and may then close the subscription. The host should make a best effort to deliver terminal status even across session restarts.

5. **Top-up idempotency**: If the same top-up is submitted multiple times (e.g. due to a retry), the host should ensure funds are only transferred once where possible. However, this is a best-effort guarantee -- products should implement their own idempotency checks for critical flows.

Expand Down
34 changes: 34 additions & 0 deletions rust/crates/truapi-codegen/tests/golden/host-callbacks.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/crates/truapi-codegen/tests/golden/wire_table.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions rust/crates/truapi/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ AutoSigning without approval. Legacy-account signing still asks the user.
`acknowledge_funding_session`, so its history writes every outcome once;
the core keeps the 50 newest recorded sessions and every unrecorded one
within the 200 newest ended.
- `PaymentPlatform`: pay from the user's balance to an account once the user
approves, and stream each payment's status by its caller-chosen id.
Installed with `set_payment_platform`; native hosts use
`set_payment_callbacks` with `notify_payment_status`. The core requires a
session. Without it, `request` and
`statusSubscribe` answer `Unsupported`.
- `TopUpPlatform`: claim a top-up source's funds into the user's balance and
stream each top-up's status. Installed with `set_top_up_platform`. The core
requires a session and checks the source keys; the host owns claiming,
Expand Down
17 changes: 9 additions & 8 deletions rust/crates/truapi/src/api/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ pub trait Payment: Send + Sync {
Subscription::interrupted(CallError::unavailable())
}

/// Request a payment from the user.
/// Request a payment from the user, followed by `id` through
/// `statusSubscribe`. Returns once the host has accepted it.
///
/// ```ts
/// // Fund the balance first so the request is not rejected for lack of funds.
Expand All @@ -50,9 +51,10 @@ pub trait Payment: Send + Sync {
/// amount: 1000n,
/// destination:
/// "0x0000000000000000000000000000000000000000000000000000000000000000",
/// id: "0x0000000000000000000000000000000000000000000000000000000000000004",
/// });
/// assert(result.isOk(), "request failed:", result);
/// console.log("payment requested:", result.value);
/// console.log("payment requested");
/// ```
#[wire(id = 2)]
async fn request(
Expand All @@ -63,7 +65,8 @@ pub trait Payment: Send + Sync {
Err(CallError::unavailable())
}

/// Subscribe to payment lifecycle updates for a specific payment.
/// Subscribe to payment lifecycle updates for a specific payment. Emits
/// the current status first, so a caller that reloads re-attaches.
///
/// ```ts
/// import { firstValueFrom, from } from "rxjs";
Expand All @@ -76,19 +79,17 @@ pub trait Payment: Send + Sync {
/// });
/// assert(topUp.isOk(), "topUp failed:", topUp);
///
/// const id = "0x0000000000000000000000000000000000000000000000000000000000000005";
/// const requested = await truapi.payment.request({
/// amount: 1000n,
/// destination:
/// "0x0000000000000000000000000000000000000000000000000000000000000000",
/// id,
/// });
/// assert(requested.isOk(), "request failed:", requested);
///
/// const status = await firstValueFrom(
/// from(
/// truapi.payment.statusSubscribe({
/// request: { paymentId: requested.value.id },
/// }),
/// ),
/// from(truapi.payment.statusSubscribe({ request: { id } })),
/// );
/// console.log("payment status received:", status);
/// ```
Expand Down
18 changes: 17 additions & 1 deletion rust/crates/truapi/src/host_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::time::Duration;

use crate::platform::{
ChatPlatform, ContactsPlatform, FundingPlatform, PermissionStatusHost, PocketPlatform,
TopUpPlatform,
PaymentPlatform, TopUpPlatform,
};
use crate::platform::{
CoreAdmin, PairingHostAdmin, PairingHostConfig, PermissionAuthorizationRequest,
Expand Down Expand Up @@ -275,6 +275,14 @@ impl PairingHostRuntime {
self.services.install_top_up_platform(platform)
}

/// Install the host's [`PaymentPlatform`], which pays from the user's
/// balance once the user approves. Set-once; returns whether this call
/// installed it.
#[instrument(skip_all, fields(runtime.method = "pairing_host_runtime.set_payment_platform"))]
pub fn set_payment_platform(&self, platform: Arc<dyn PaymentPlatform>) -> bool {
self.services.install_payment_platform(platform)
}

/// Install the host's [`FundingPlatform`], the native funding overlay.
///
/// Set-once. Returns whether this call installed it. Call it before
Expand Down Expand Up @@ -735,6 +743,14 @@ impl SigningHostRuntime {
self.services.install_top_up_platform(platform)
}

/// Install the host's [`PaymentPlatform`], which pays from the user's
/// balance once the user approves. Set-once; returns whether this call
/// installed it.
#[instrument(skip_all, fields(runtime.method = "signing_host_runtime.set_payment_platform"))]
pub fn set_payment_platform(&self, platform: Arc<dyn PaymentPlatform>) -> bool {
self.services.install_payment_platform(platform)
}

/// Install the host's [`FundingPlatform`], the native funding overlay.
///
/// Set-once. Returns whether this call installed it. Call it before
Expand Down
10 changes: 10 additions & 0 deletions rust/crates/truapi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,16 @@ pub mod latest {
/// Failure watching a funding session.
pub type HostFundingStatusSubscribeError =
LatestOf<versioned::funding::HostFundingStatusSubscribeError>;
/// Payment request.
pub type HostPaymentRequest = LatestOf<versioned::payment::HostPaymentRequest>;
/// Payment request failure.
pub type HostPaymentError = LatestOf<versioned::payment::HostPaymentError>;
/// Progress of a payment.
pub type HostPaymentStatusSubscribeItem =
LatestOf<versioned::payment::HostPaymentStatusSubscribeItem>;
/// Failure following a payment.
pub type HostPaymentStatusSubscribeError =
LatestOf<versioned::payment::HostPaymentStatusSubscribeError>;
/// Payment top-up request.
pub type HostPaymentTopUpRequest = LatestOf<versioned::payment::HostPaymentTopUpRequest>;
/// Payment top-up failure.
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/truapi/src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub use crate::host_internal::sso_messages::SsoRequestOutcome;
pub use crate::host_logic::dotns::{NavigateDecision, PocketDeeplinkAction};
pub use callbacks::{
HostCallbacks, NativeChatCallbacks, NativeContactsCallbacks, NativeFundingCallbacks,
NativePocketCallbacks, NativePocketRemoval, NativeTopUpCallbacks,
NativePaymentCallbacks, NativePocketCallbacks, NativePocketRemoval, NativeTopUpCallbacks,
};
pub use config::{HostRuntimeConfig, NativeRuntimeConfigError, ProductExecutionConfig};
pub use errors::{HostRejection, NativeCoreDatabaseError};
Expand Down
29 changes: 29 additions & 0 deletions rust/crates/truapi/src/native/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,35 @@ pub trait NativeFundingCallbacks: Send + Sync {
fn funding_session_changed(&self, intent: String, status: v01::HostFundingStatusSubscribeItem);
}

/// Native payment engine, which pays from the user's balance to an account.
/// A host passes an implementation to
/// [`NativeTrUApiHostRuntime::set_payment_callbacks`] and reports each later
/// status with [`NativeTrUApiHostRuntime::notify_payment_status`].
///
/// [`NativeTrUApiHostRuntime::set_payment_callbacks`]: super::NativeTrUApiHostRuntime::set_payment_callbacks
/// [`NativeTrUApiHostRuntime::notify_payment_status`]: super::NativeTrUApiHostRuntime::notify_payment_status
#[uniffi::export(rust, foreign)]
#[async_trait::async_trait]
pub trait NativePaymentCallbacks: Send + Sync {
/// Ask the user to approve payment `request` for `product_id`, returning
/// once the user has decided: `Ok` when they authorized it and the host
/// took it on. Ids are scoped to `product_id`. Its amount is a decimal
/// string of CASH units.
async fn request_payment(
&self,
product_id: String,
request: v01::HostPaymentRequest,
) -> Result<(), v01::HostPaymentError>;

/// The current status of `product_id`'s payment `id`, or `None` when the
/// host holds no such payment.
fn payment_status(
&self,
product_id: String,
id: crate::Bytes32,
) -> Result<Option<v01::HostPaymentStatusSubscribeItem>, HostRejection>;
}

/// Native top-up engine, which claims a source's funds into the user's
/// balance. A host passes an implementation to
/// [`NativeTrUApiHostRuntime::set_top_up_callbacks`] and reports each later
Expand Down
Loading
Loading