diff --git a/snippets/csharp/Metadata.cs b/snippets/csharp/Metadata.cs new file mode 100644 index 00000000..4420c5a3 --- /dev/null +++ b/snippets/csharp/Metadata.cs @@ -0,0 +1,78 @@ +using Breez.Sdk; +using System.Text.Json; + +public class MetadataSnippets +{ + public void SetPaymentMetadata(BlockingBreezServices sdk) + { + // ANCHOR: set-payment-metadata + sdk.SetPaymentMetadata("target-payment-hash", "{\"myCustomValue\":true}"); + // ANCHOR_END: set-payment-metadata + } + + public void FilterPaymentMetadata(BlockingBreezServices sdk) + { + // ANCHOR: filter-payment-metadata + try + { + var metadataFilters = new List() { + new MetadataFilter( + jsonPath: "myCustomValue", + jsonValue: "true" + ) + }; + + var payments = sdk.ListPayments( + new ListPaymentsRequest( + metadataFilters: metadataFilters + ) + ); + } + catch (Exception) + { + // Handle error + } + // ANCHOR_END: filter-payment-metadata + } + + public void FilterPaymentMetadataString(BlockingBreezServices sdk) + { + // ANCHOR: filter-payment-metadata-string + var metadataFilters = new List() { + new MetadataFilter( + jsonPath: "customerName", + jsonValue: "\"Satoshi Nakamoto\"" + ), + new MetadataFilter( + jsonPath: "customerName", + jsonValue: JsonSerializer.Serialize("Satoshi Nakamoto") + ) + }; + // ANCHOR_END: filter-payment-metadata-string + } + + public void FilterPaymentMetadataObject(BlockingBreezServices sdk) + { + // ANCHOR: filter-payment-metadata-object + // This will *NOT* work + var _metadataFilters = new List() { + new MetadataFilter( + jsonPath: "parent.nestedArray", + jsonValue: "[1, 2, 3]" + ) + }; + + // Any of these will work + var metadataFilters = new List() { + new MetadataFilter( + jsonPath: "parent.nestedArray", + jsonValue: "[1,2,3]" + ), + new MetadataFilter( + jsonPath: "parent.nestedArray", + jsonValue: JsonSerializer.Serialize(new int[] {1, 2, 3}) + ) + }; + // ANCHOR_END: filter-payment-metadata-object + } +} diff --git a/snippets/dart_snippets/lib/metadata.dart b/snippets/dart_snippets/lib/metadata.dart new file mode 100644 index 00000000..dd54fe69 --- /dev/null +++ b/snippets/dart_snippets/lib/metadata.dart @@ -0,0 +1,51 @@ +import 'package:breez_sdk/breez_sdk.dart'; +import 'package:breez_sdk/bridge_generated.dart'; + +Future setPaymentMetadata({required String paymentHash, required String metadata}) async { + // ANCHOR: set-payment-metadata + await BreezSDK().setPaymentMetadata(hash: "target-payment-hash", metadata: '{"myCustomValue":true}'); + // ANCHOR_END: set-payment-metadata +} + +Future filterPaymentMetadata() async { + // ANCHOR: filter-payment-metadata + List metadataFilters = [ + MetadataFilter( + jsonPath: "myCustomValue", + jsonValue: "true", + ), + ]; + + await BreezSDK().listPayments( + req: ListPaymentsRequest( + metadataFilters: metadataFilters + )); + // ANCHOR_END: filter-payment-metadata + + // ANCHOR: filter-payment-metadata-string + metadataFilters = [ + MetadataFilter( + jsonPath: "customerName", + jsonValue: '"Satoshi Nakamoto"', + ), + ]; + // ANCHOR_END: filter-payment-metadata-string + + // ANCHOR: filter-payment-metadata-object + // This will *NOT* work + metadataFilters = [ + MetadataFilter( + jsonPath: "parent.nestedArray", + jsonValue: "[1, 2, 3]", + ), + ]; + + // Any of these will work + metadataFilters = [ + MetadataFilter( + jsonPath: "parent.nestedArray", + jsonValue: "[1,2,3]", + ), + ]; + // ANCHOR_END: filter-payment-metadata-object +} diff --git a/snippets/go/metadata.go b/snippets/go/metadata.go new file mode 100644 index 00000000..97d5b7d5 --- /dev/null +++ b/snippets/go/metadata.go @@ -0,0 +1,72 @@ +package example + +import ( + "encoding/json" + "log" + + "github.com/breez/breez-sdk-go/breez_sdk" +) + +func SetPaymentMetadata() { + // ANCHOR: set-payment-metadata + sdk.SetPaymentMetadata("target-payment-hash", `{"myCustomValue":true}`) + // ANCHOR_END: set-payment-metadata +} + +func FilterPaymentMetadata() { + // ANCHOR: filter-payment-metadata + metadataFilters := []breez_sdk.MetadataFilter{ + {JsonPath: "myCustomValue", JsonValue: "true"}, + } + + payments, err := sdk.ListPayments(breez_sdk.ListPaymentsRequest{ + MetadataFilters: &metadataFilters, + }) + + if err != nil { + // handle error + } + // ANCHOR_END: filter-payment-metadata + log.Printf("%#v", payments) +} + +func FilterPaymentMetadataString() { + // ANCHOR: filter-payment-metadata-string + metadataFilters := []breez_sdk.MetadataFilter{ + {JsonPath: "customerName", JsonValue: "\"Satoshi Nakamoto\""}, + } + + jsonValue, _ := json.Marshal("Satoshi Nakamoto") + metadataFilters = []breez_sdk.MetadataFilter{ + { + JsonPath: "customerName", + JsonValue: string(jsonValue), + }, + } + // ANCHOR_END: filter-payment-metadata-string + + sdk.ListPayments(breez_sdk.ListPaymentsRequest{ + MetadataFilters: &metadataFilters, + }) +} + +func FilterPaymentMetadataObject() { + // ANCHOR: filter-payment-metadata-object + // This will *NOT* work + metadataFilters := []breez_sdk.MetadataFilter{ + {JsonPath: "parent.nestedArray", JsonValue: "[1, 2, 3]"}, + } + + // Any of these will work + jsonValue, _ := json.Marshal([]int{1, 2, 3}) + + metadataFilters = []breez_sdk.MetadataFilter{ + {JsonPath: "parent.nestedArray", JsonValue: "[1,2,3]"}, + {JsonPath: "parent.nestedArray", JsonValue: string(jsonValue)}, + } + // ANCHOR_END: filter-payment-metadata-object + + sdk.ListPayments(breez_sdk.ListPaymentsRequest{ + MetadataFilters: &metadataFilters, + }) +} diff --git a/snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/Metadata.kt b/snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/Metadata.kt new file mode 100644 index 00000000..3097ded6 --- /dev/null +++ b/snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/Metadata.kt @@ -0,0 +1,57 @@ +package com.example.kotlinmpplib + +import breez_sdk.* +class Metadata { + fun SetPaymentMetadata(sdk: BlockingBreezServices) { + // ANCHOR: set-payment-metadata + try { + sdk.setPaymentMetadata("target-payment-hash", """{"myCustomValue":true}""") + } catch (e: Exception) { + // Handle error + } + // ANCHOR_END: set-payment-metadata + } + + fun FilterPaymentMetadata(sdk: BlockingBreezServices) { + // ANCHOR: filter-payment-metadata + val metadataFilters = listOf(MetadataFilter( + jsonPath = "myCustomValue", + jsonValue = "true" + )) + + try { + sdk.listPayments( + ListPaymentsRequest( + metadataFilters = metadataFilters + )) + } catch (e: Exception) { + // handle error + } + // ANCHOR_END: filter-payment-metadata + } + + fun FilterPaymentMetadataString(sdk: BlockingBreezServices) { + // ANCHOR: filter-payment-metadata-string + val metadataFilters = listOf(MetadataFilter( + jsonPath = "customerName", + jsonValue = "\"Satoshi Nakamoto\"" + )) + // ANCHOR_END: filter-payment-metadata-string + } + + fun FilterPaymentMetadataObject(sdk: BlockingBreezServices) { + // ANCHOR: filter-payment-metadata-object + // This will *NOT* work + val _metadataFilters = listOf(MetadataFilter( + jsonPath = "parent.nestedArray", + jsonValue = "[1, 2, 3]" + )) + + // Any of these will work + val metadataFilters = listOf(MetadataFilter( + jsonPath = "parent.nestedArray", + jsonValue = "[1,2,3]" + )) + // ANCHOR_END: filter-payment-metadata-object + } +} diff --git a/snippets/python/src/metadata.py b/snippets/python/src/metadata.py new file mode 100644 index 00000000..ebab54c2 --- /dev/null +++ b/snippets/python/src/metadata.py @@ -0,0 +1,45 @@ +import breez_sdk + +def set_payment_metadata(sdk_services): + try: + # ANCHOR: set-payment-metadata + sdk_services.set_payment_metadata("target-payment-hash", '{"myCustomValue":true}') + # ANCHOR_END: set-payment-metadata + except Exception as error: + # handle error + raise + +def filter_payment_metadata(sdk_services): + # ANCHOR: filter-payment-metadata + metadata_filters = [ + breez_sdk.MetadataFilter("myCustomValue", "true") + ] + + try: + sdk_services.list_payments(breez_sdk.ListPaymentsRequest( + metadata_filters = metadata_filters + )) + except Exception as error: + # handle error + raise + # ANCHOR_END: filter-payment-metadata + + # ANCHOR: filter-payment-metadata-string + metadata_filters = [ + breez_sdk.MetadataFilter("customerName", "\"Satoshi Nakamoto\""), + breez_sdk.MetadataFilter("customerName", json.dumps("Satoshi Nakamoto")), + ] + # ANCHOR_END: filter-payment-metadata-string + + # ANCHOR: filter-payment-metadata-object + # This will *NOT* work + metadata_filters = [ + breez_sdk.MetadataFilter("parent.nestedArray", "[1, 2, 3]") + ] + + # Any of these will work + metadata_filters = [ + breez_sdk.MetadataFilter("parent.nestedArray", "[1,2,3]"), + breez_sdk.MetadataFilter("parent.nestedArray", json.dumps([1,2,3], separators=(',', ':'))), + ] + # ANCHOR_END: filter-payment-metadata-object diff --git a/snippets/react-native/metadata.ts b/snippets/react-native/metadata.ts new file mode 100644 index 00000000..efeedbcc --- /dev/null +++ b/snippets/react-native/metadata.ts @@ -0,0 +1,66 @@ +import { setPaymentMetadata, listPayments } from '@breeztech/react-native-breez-sdk' + +const testSetPaymentMetadata = async () => { + // ANCHOR: set-payment-metadata + await setPaymentMetadata('target-payment-hash', '{"myCustomValue":true}') + // ANCHOR_END: set-payment-metadata +} + +const testFilterPaymentMetadata = async () => { + // ANCHOR: filter-payment-metadata + const metadataFilters = [ + { + jsonPath: 'myCustomValue', + jsonValue: 'true' + } + ] + + try { + await listPayments({ + metadataFilters + }) + } catch (err) { + // handle error + } + // ANCHOR_END: filter-payment-metadata +} + +const testFilterPaymentMetadataString = async () => { + // ANCHOR: filter-payment-metadata-string + // Note: These are equivalent + const metadataFilters = [ + { + jsonPath: 'customerName', + jsonValue: 'Satoshi Nakamoto' + }, + { + jsonPath: 'customerName', + jsonValue: JSON.stringify('Satoshi Nakamoto') + } + ] + // ANCHOR_END: filter-payment-metadata-string +} + +const testFilterPaymentMetadataObject = async () => { + // ANCHOR: filter-payment-metadata-object + // This will *NOT* work + const _metadataFilters = [ + { + jsonPath: 'parent.nestedArray', + jsonValue: '[1, 2, 3]' + } + ] + + // Any of these will work + const metadataFilters = [ + { + jsonPath: 'parent.nestedArray', + jsonValue: '[1,2,3]' + }, + { + jsonPath: 'parent.nestedArray', + jsonValue: JSON.stringify([1, 2, 3]) + } + ] + // ANCHOR_END: filter-payment-metadata-object +} diff --git a/snippets/rust/src/metadata.rs b/snippets/rust/src/metadata.rs new file mode 100644 index 00000000..f06c17f5 --- /dev/null +++ b/snippets/rust/src/metadata.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; +use anyhow::Result; +use breez_sdk_core::*; + +async fn set_payment_metadata(sdk: Arc) -> Result<()> { + // ANCHOR: set-payment-metadata + sdk.set_payment_metadata("target-payment-hash", r#"{"myCustomValue":true}"#).await?; + // ANCHOR_END: set-payment-metadata + + Ok(()) +} + +async fn filter_payment_metadata(sdk: Arc) -> Result<()> { + // ANCHOR: filter-payment-metadata + let metadata_filters = vec![ + MetadataFilter { + json_path: "myCustomValue".to_string(), + json_value: "true".to_string(), + }, + ]; + + sdk.list_payments(ListPaymentsRequest { + metadata_filters, + ..Default::default(), + }).await?; + // ANCHOR_END: filter-payment-metadata +} + +async fn filter_payment_metadata_string(sdk: Arc) -> Result<()> { + // ANCHOR: filter-payment-metadata-string + // Note: The following are equivalent + let metadata_filters = vec![ + MetadataFilter { + json_path: "customerName".to_string(), + json_value: r#""Satoshi Nakamoto""#.to_string(), + }, + MetadataFilter { + json_path: "customerName".to_string(), + json_value: serde_json::json!("Satoshi Nakamoto").to_string(), + }, + ]; + // ANCHOR_END: filter-payment-metadata-string +} + +async fn filter_payment_metadata_object(sdk: Arc) -> Result<()> { + // ANCHOR: filter-payment-metadata-object + // This will *NOT* work + let metadata_filters = vec![ + MetadataFilter { + json_path: "parent.nestedArray".to_string(), + json_value: r#"[1, 2, 3]"#.to_string(), + }, + ]; + + // Any of these will work + let metadata_filters = vec![ + MetadataFilter { + json_path: "parent.nestedArray".to_string(), + json_value: r#"[1,2,3]"#.to_string(), + }, + ]; + // ANCHOR_END: filter-payment-metadata-object + + // ANCHOR: filter-payment-metadata-object-serde + let metadata_filters = vec![ + MetadataFilter { + json_path: "parent.nestedArray".to_string(), + json_value: serde_json::json!(&[1, 2, 3]).to_string(), + }, + ]; + // ANCHOR_END: filter-payment-metadata-object-serde + Ok(()) +} diff --git a/snippets/swift/BreezSDKExamples/Sources/Metadata.swift b/snippets/swift/BreezSDKExamples/Sources/Metadata.swift new file mode 100644 index 00000000..0141533d --- /dev/null +++ b/snippets/swift/BreezSDKExamples/Sources/Metadata.swift @@ -0,0 +1,76 @@ +// +// Metadata.swift +// +// +// + +import Foundation +import BreezSDK + +func SetPaymentMetadata(sdk: BlockingBreezServices) throws { + // ANCHOR: set-payment-metadata + try sdk.setPaymentMetadata(hash: "target-payment-hash", metadata: #"{"myCustomValue":true}"#) + // ANCHOR_END: set-payment-metadata +} + +func FilterPaymentMetadata(sdk: BlockingBreezServices) -> [Payment]? { + // ANCHOR: filter-payment-metadata + let metadataFilters = [ + MetadataFilter( + jsonPath: "myCustomValue", + jsonValue: "true" + ) + ] + + let payments = try? sdk.listPayments( + req: ListPaymentsRequest( + metadataFilters: metadataFilters + ) + ) + // ANCHOR_END: filter-payment-metadata + + return payments +} + +func FilterPaymentMetadataString(sdk: BlockingBreezServices) -> [Payment]? { + // ANCHOR: filter-payment-metadata-string + let metadataFilters = [ + MetadataFilter( + jsonPath: "myCustomValue", + jsonValue: #""true""# + ) + ] + // ANCHOR_END: filter-payment-metadata-string + + return try? sdk.listPayments( + req: ListPaymentsRequest( + metadataFilters: metadataFilters + ) + ) +} + +func FilterPaymentMetadataObject(sdk: BlockingBreezServices) -> [Payment]? { + // ANCHOR: filter-payment-metadata-object + // This will *NOT* work + var metadataFilters = [ + MetadataFilter( + jsonPath: "myCustomValue", + jsonValue: #"[1, 2, 3]"# + ) + ] + + // Any of these will work + metadataFilters = [ + MetadataFilter( + jsonPath: "myCustomValue", + jsonValue: #"[1,2,3]"# + ) + ] + // ANCHOR_END: filter-payment-metadata-object + + return try? sdk.listPayments( + req: ListPaymentsRequest( + metadataFilters: metadataFilters + ) + ) +} diff --git a/src/SUMMARY.md b/src/SUMMARY.md index e972c7ec..f77f7a96 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -12,6 +12,7 @@ - [Receiving payments via mobile notifications](guide/payment_notification.md) - [iOS](guide/ios_notification_service_extension.md) - [Android](guide/android_notification_foreground_service.md) + - [Adding and filtering for payment metadata](guide/payment_metadata.md) - [Connecting to an LSP](guide/connecting_lsp.md) - [Receiving an On-Chain Transaction](guide/receive_onchain.md) - [Sending an On-Chain Transaction](guide/send_onchain.md) diff --git a/src/guide/payment_metadata.md b/src/guide/payment_metadata.md new file mode 100644 index 00000000..18e3e90e --- /dev/null +++ b/src/guide/payment_metadata.md @@ -0,0 +1,326 @@ +# Adding and filtering for payment metadata + +## Usage + +As developers, we understand the necessity to associate external metadata to a certain payment. The Breez SDK allows you to easily do so with the `set_payment_metadata` method: + + +
Rust
+
+ +```rust,ignore +{{#include ../../snippets/rust/src/metadata.rs:set-payment-metadata}} +``` +
+ +
Swift
+
+ +```swift,ignore +{{#include ../../snippets/swift/BreezSDKExamples/Sources/Metadata.swift:set-payment-metadata}} +``` +
+ +
Kotlin
+
+ +```kotlin,ignore +{{#include ../../snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/Metadata.kt:set-payment-metadata}} +``` +
+ +
React Native
+
+ +```typescript +{{#include ../../snippets/react-native/metadata.ts:set-payment-metadata}} +``` +
+ +
Dart
+
+ +```dart,ignore +{{#include ../../snippets/dart_snippets/lib/metadata.dart:set-payment-metadata}} +``` +
+ +
Python
+
+ +```python,ignore +{{#include ../../snippets/python/src/metadata.py:set-payment-metadata}} +``` +
+ +
Go
+
+ +```go,ignore +{{#include ../../snippets/go/metadata.go:set-payment-metadata}} +``` +
+ +
C#
+
+ +```cs,ignore +{{#include ../../snippets/csharp/Metadata.cs:set-payment-metadata}} +``` +
+ +
+ +Once the metadata has been set, you can filter for the specified value using the `list_payments` method: + + +
Rust
+
+ +```rust,ignore +{{#include ../../snippets/rust/src/metadata.rs:filter-payment-metadata}} +``` +
+ +
Swift
+
+ +```swift,ignore +{{#include ../../snippets/swift/BreezSDKExamples/Sources/Metadata.swift:filter-payment-metadata}} +``` +
+ +
Kotlin
+
+ +```kotlin,ignore +{{#include ../../snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/Metadata.kt:filter-payment-metadata}} +``` +
+ +
React Native
+
+ +```typescript +{{#include ../../snippets/react-native/metadata.ts:filter-payment-metadata}} +``` +
+ +
Dart
+
+ +```dart,ignore +{{#include ../../snippets/dart_snippets/lib/metadata.dart:filter-payment-metadata}} +``` +
+ +
Python
+
+ +```python,ignore +{{#include ../../snippets/python/src/metadata.py:filter-payment-metadata}} +``` +
+ +
Go
+
+ +```go,ignore +{{#include ../../snippets/go/metadata.go:filter-payment-metadata}} +``` +
+ +
C#
+
+ +```cs,ignore +{{#include ../../snippets/csharp/Metadata.cs:filter-payment-metadata}} +``` +
+ +
+ +## Caveats + +Searching for metadata is flexible, allowing you to search and compare all JSON-supported types (nested ones too, using [JSONPath](https://www.ibm.com/docs/en/netezza?topic=ddt-jsonpath)), but with a couple of caveats: + +### 2.1 Filtering for Strings + +Since the filter works as a one-to-one comparison to the JSON value, strings **must** be wrapped in double-quotes in order to be properly filtered: + + +
Rust
+
+ +```rust,ignore +{{#include ../../snippets/rust/src/metadata.rs:filter-payment-metadata-string}} +``` + +
+ +
Swift
+
+ +```swift,ignore +{{#include ../../snippets/swift/BreezSDKExamples/Sources/Metadata.swift:filter-payment-metadata-string}} +``` +
+ +
Kotlin
+
+ +```kotlin,ignore +{{#include ../../snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/Metadata.kt:filter-payment-metadata-string}} +``` +
+ +
React Native
+
+ +```typescript +{{#include ../../snippets/react-native/metadata.ts:filter-payment-metadata-string}} +``` +
+ +
Dart
+
+ +```dart,ignore +{{#include ../../snippets/dart_snippets/lib/metadata.dart:filter-payment-metadata-string}} +``` +
+ +
Python
+
+ +```python,ignore +{{#include ../../snippets/python/src/metadata.py:filter-payment-metadata-string}} +``` +
+ +
Go
+
+ +```go,ignore +{{#include ../../snippets/go/metadata.go:filter-payment-metadata-string}} +``` +
+ +
C#
+
+ +```cs,ignore +{{#include ../../snippets/csharp/Metadata.cs:filter-payment-metadata-string}} +``` +
+ +
+ +### 2.2 Filtering for Objects/Arrays + +You can also compare complex objects against one another, but be careful of whitespaces! Since those are stripped during insertion, passing non-stripped filters will result in improper matching. For example, given the following metadata: + +
+
+{
+ "isNested": true,
+ "parent": {  
+  "nestedArray": [1, 2, 3]
+ }
+}
+
+
+ +You would filter for payments matching the nested array as follows: + + +
Rust
+
+ +```rust,ignore +{{#include ../../snippets/rust/src/metadata.rs:filter-payment-metadata-object}} +``` + +In Rust's case, this check can easily be overcome by using the [serde_json](https://docs.rs/serde_json/latest/serde_json/) crate (which you probably should be using anyway to serialize and insert the metadata): + +```rust,ignore +{{#include ../../snippets/rust/src/metadata.rs:filter-payment-metadata-object-serde}} +``` +
+ +
Swift
+
+ +```swift,ignore +{{#include ../../snippets/swift/BreezSDKExamples/Sources/Metadata.swift:filter-payment-metadata-object}} +``` +
+ +
Kotlin
+
+ +```kotlin,ignore +{{#include ../../snippets/kotlin_mpp_lib/shared/src/commonMain/kotlin/com/example/kotlinmpplib/Metadata.kt:filter-payment-metadata-object}} +``` +
+ +
React Native
+
+ +```typescript +{{#include ../../snippets/react-native/metadata.ts:filter-payment-metadata-object}} +``` +
+ +
Dart
+
+ +```dart,ignore +{{#include ../../snippets/dart_snippets/lib/metadata.dart:filter-payment-metadata-object}} +``` +
+ +
Python
+
+ +```python,ignore +{{#include ../../snippets/python/src/metadata.py:filter-payment-metadata-object}} +``` +
+ +
Go
+
+ +```go,ignore +{{#include ../../snippets/go/metadata.go:filter-payment-metadata-object}} +``` +
+ +
C#
+
+ +```cs,ignore +{{#include ../../snippets/csharp/Metadata.cs:filter-payment-metadata-object}} +``` +
+ +
+ +### 2.3 Same-key Insertion + +In case the same key were to be specified twice during insertion, the _last_ one occurring in the string is taken as valid by default. E.g. + +
+
+{
+ "completed": true,
+ "completed": false
+}
+
+
+ +will insert the value as `false`. + +### 2.4 Size Limits + +Currently, the SDK limits metadata storage per payment to 1,000 UTF-8 encoded characters, and any insertion beyond that will fail.