Skip to content
Merged
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
63 changes: 57 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ If you are an AI agent implementing this SDK, follow the steps below exactly. If

If they are on the receive side, ask one follow-up:
- **Platform** — single-tenant, one API key.
- **Parent Platform** — multi-tenant, manages multiple child platforms each with their own API key and HMAC secret.
- **Parent Platform** — multi-tenant, manages multiple child platforms. Two variants:
- **Per-client keys** — each child has its own API key and HMAC secret; you scope requests per-call.
- **Shared key** — one API key and HMAC secret for all children; tag the child per-payment with `setChildPlatform()`.

**2. Follow the matching Quick Start section below.**

Expand All @@ -47,9 +49,13 @@ Receive side (platforms):
- Always call `.setZk()` on the `PaymentBuilder` before calling `addPaymentAsync`. Plain-text destinations are rejected in `strict` mode.
- Store the `secret` from `AddPaymentResult` alongside the invoice — it is required to reconstruct the verify URL for the wallet.

Receive side (parent platforms), in addition to the platform rules:
- Include `hmacSecret` in `BrantaClientOptions`.
- Pass per-call `BrantaClientOptions` to scope requests to the correct child platform's API key.
Receive side (parent platforms — per-client keys), in addition to the platform rules:
- Include `hmacSecret` in `BrantaClientOptions` but omit `defaultApiKey` at service setup.
- Pass per-call `BrantaClientOptions` with each child's API key to scope requests.

Receive side (parent platforms — shared key), in addition to the platform rules:
- Include both `defaultApiKey` and `hmacSecret` in `BrantaClientOptions`.
- Call `.setChildPlatform(name, logoUrl: ..., logoLightUrl: ...)` on the builder to tag each payment with the child's branding.

# Quick Start

Expand Down Expand Up @@ -155,15 +161,20 @@ final result = await service.addPaymentAsync(payment);

## For Parent Platforms (Receive Side)

Parent platforms sign requests with HMAC in addition to the API key. Pass per-call `BrantaClientOptions` to scope requests to each child platform.
Parent platforms sign requests with HMAC. Choose a variant based on how API keys are structured.

<details>
<summary>Shared key — one API key covers all children (Recommended)</summary>

Set up with a single API key and HMAC secret; identify the child platform per-payment.

```dart
import 'package:branta/branta.dart';
import 'package:http/http.dart' as http;

final options = BrantaClientOptions(
baseUrl: BrantaServerBaseUrl.staging,
defaultApiKey: '<api-key>',
defaultApiKey: '<shared-api-key>',
hmacSecret: '<hmac-secret>',
privacy: PrivacyMode.strict,
);
Expand All @@ -178,12 +189,52 @@ final payment = PaymentBuilder()
.setDescription('Order #1234')
.addDestination('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')
.setZk()
.setChildPlatform('ChildBrand', logoUrl: 'https://example.com/logo.png')
.setTtl(600)
.build();

final result = await service.addPaymentAsync(payment);
```

</details>

<details>
<summary>Per-client keys — each child has its own API key</summary>

Set up the service with the shared HMAC secret only; pass each child's API key per-call.

```dart
import 'package:branta/branta.dart';
import 'package:http/http.dart' as http;

final options = BrantaClientOptions(
baseUrl: BrantaServerBaseUrl.staging,
hmacSecret: '<hmac-secret>',
privacy: PrivacyMode.strict,
);
final brantaClient = BrantaClient(httpClient: http.Client(), defaultOptions: options);
final service = BrantaService(
client: brantaClient,
aesEncryption: AesEncryptionService(),
defaultOptions: options,
);

final payment = PaymentBuilder()
.setDescription('Order #1234')
.addDestination('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')
.setZk()
.setTtl(600)
.build();

// Scope to the child platform's API key per-call
final result = await service.addPaymentAsync(
payment,
options: BrantaClientOptions(defaultApiKey: '<child-api-key>'),
);
```

</details>

# Privacy

`PrivacyMode` controls whether plain-text on-chain lookups are allowed.
Expand Down
6 changes: 6 additions & 0 deletions lib/src/v2/classes/payment_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:uuid/uuid.dart';
import '../../enums/destination_type.dart';
import '../models/destination.dart';
import '../models/payment.dart';
import '../models/platform.dart';

class PaymentBuilder {
final Payment _payment = Payment(destinations: []);
Expand Down Expand Up @@ -50,5 +51,10 @@ class PaymentBuilder {
return this;
}

PaymentBuilder setChildPlatform(String name, {String? logoUrl, String? logoLightUrl}) {
_payment.childPlatform = Platform(name: name, logoUrl: logoUrl, logoLightUrl: logoLightUrl);
return this;
}

Payment build() => _payment;
}
3 changes: 3 additions & 0 deletions lib/src/v2/models/payment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Payment {
String? platformLogoUrl;
String? platformLogoLightUrl;
Platform? parentPlatform;
Platform? childPlatform;
String? btcPayServerPluginVersion;

/// Runtime-only flag set by [BrantaService] after metadata decryption.
Expand All @@ -27,6 +28,7 @@ class Payment {
this.platformLogoUrl,
this.platformLogoLightUrl,
this.parentPlatform,
this.childPlatform,
this.btcPayServerPluginVersion,
});

Expand Down Expand Up @@ -67,6 +69,7 @@ class Payment {
if (platformLogoLightUrl != null)
'platform_logo_light_url': platformLogoLightUrl,
if (parentPlatform != null) 'parent_platform': parentPlatform!.toJson(),
if (childPlatform != null) 'child_platform': childPlatform!.toJson(),
if (btcPayServerPluginVersion != null)
'btc_pay_server_plugin_version': btcPayServerPluginVersion,
};
Expand Down
62 changes: 62 additions & 0 deletions test/branta_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,68 @@ void main() {
final dest = Destination(value: 'addr');
expect(dest.toJson().containsKey('type'), isFalse);
});

test('setChildPlatform sets name', () {
final payment = PaymentBuilder()
.addDestination(_bitcoinAddress)
.setChildPlatform('Acme')
.build();

expect(payment.childPlatform!.name, equals('Acme'));
});

test('setChildPlatform optional URLs default to null', () {
final payment = PaymentBuilder()
.addDestination(_bitcoinAddress)
.setChildPlatform('Acme')
.build();

expect(payment.childPlatform!.logoUrl, isNull);
expect(payment.childPlatform!.logoLightUrl, isNull);
});

test('setChildPlatform with URLs sets urls', () {
final payment = PaymentBuilder()
.addDestination(_bitcoinAddress)
.setChildPlatform('Acme',
logoUrl: 'https://example.com/logo.png',
logoLightUrl: 'https://example.com/logo-light.png')
.build();

expect(payment.childPlatform!.logoUrl, equals('https://example.com/logo.png'));
expect(payment.childPlatform!.logoLightUrl, equals('https://example.com/logo-light.png'));
});

test('setChildPlatform returns builder', () {
final builder = PaymentBuilder().addDestination(_bitcoinAddress);
final result = builder.setChildPlatform('Acme');

expect(result, same(builder));
});

test('setChildPlatform serializes to toJson', () {
final payment = PaymentBuilder()
.addDestination(_bitcoinAddress)
.setChildPlatform('Acme', logoUrl: 'https://example.com/logo.png')
.build();

final json = payment.toJson();
final cp = json['child_platform'] as Map<String, dynamic>;

expect(cp['name'], equals('Acme'));
expect(cp['logo_url'], equals('https://example.com/logo.png'));
expect(cp.containsKey('logo_light_url'), isFalse);
});

test('no childPlatform omitted from toJson', () {
final payment = PaymentBuilder()
.addDestination(_bitcoinAddress)
.build();

final json = payment.toJson();

expect(json.containsKey('child_platform'), isFalse);
});
});

// -------------------------------------------------------------------------
Expand Down
Loading