@branta-ops/branta 1.0.2 → 3.0.1
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.
- package/README.md +8 -0
- package/dist/classes/brantaClientOptions.d.ts +15 -0
- package/dist/helpers/aes.d.ts +1 -1
- package/dist/helpers/aes.js +10 -2
- package/dist/helpers/hashZk.d.ts +3 -0
- package/dist/helpers/hashZk.js +13 -0
- package/dist/index.d.ts +3 -6
- package/dist/index.js +1 -4
- package/dist/v2/client.d.ts +4 -34
- package/dist/v2/client.js +7 -98
- package/dist/v2/index.d.ts +3 -0
- package/dist/v2/index.js +2 -0
- package/dist/v2/service.d.ts +19 -0
- package/dist/v2/service.js +211 -0
- package/dist/v2/types.d.ts +38 -0
- package/dist/v2/types.js +1 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ import { V2BrantaClient, BrantaServerBaseUrl } from "@branta-ops/branta";
|
|
|
18
18
|
|
|
19
19
|
const client = new V2BrantaClient({
|
|
20
20
|
baseUrl: BrantaServerBaseUrl.Production,
|
|
21
|
+
privacy: 'loose',
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
await client.getPayments("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
|
|
@@ -31,6 +32,7 @@ import { V2BrantaClient, BrantaServerBaseUrl } from "@branta-ops/branta";
|
|
|
31
32
|
const client = new V2BrantaClient({
|
|
32
33
|
baseUrl: BrantaServerBaseUrl.Production,
|
|
33
34
|
defaultApiKey: "<default-api-key>",
|
|
35
|
+
privacy: 'loose',
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
await client.addPayment({
|
|
@@ -54,6 +56,7 @@ const client = new V2BrantaClient({
|
|
|
54
56
|
baseUrl: BrantaServerBaseUrl.Production,
|
|
55
57
|
defaultApiKey: "<default-api-key>",
|
|
56
58
|
hmacSecret: "<hmac-secret>",
|
|
59
|
+
privacy: 'loose',
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
await client.addPayment({
|
|
@@ -68,6 +71,11 @@ await client.addPayment({
|
|
|
68
71
|
});
|
|
69
72
|
```
|
|
70
73
|
|
|
74
|
+
## Release
|
|
75
|
+
- npm login
|
|
76
|
+
- npm version major|minor|patch
|
|
77
|
+
- npm publish
|
|
78
|
+
|
|
71
79
|
## Feature Support
|
|
72
80
|
|
|
73
81
|
- [X] Per Environment configuration
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { ServerEnvironment } from "./brantaServerBaseUrl.js";
|
|
2
|
+
/**
|
|
3
|
+
* Controls the privacy posture for on-chain address lookups.
|
|
4
|
+
*
|
|
5
|
+
* - `'strict'` — Only ZK (zero-knowledge / encrypted) on-chain lookups are
|
|
6
|
+
* permitted. Calling `getPayments` directly will throw a
|
|
7
|
+
* `BrantaPaymentException`; plain-address branches inside
|
|
8
|
+
* `getPaymentByQrCode` will silently return `[]`. Lightning invoices and
|
|
9
|
+
* all POST operations are unaffected by this setting.
|
|
10
|
+
*
|
|
11
|
+
* - `'loose'` — Both plain and ZK on-chain lookups are allowed. No
|
|
12
|
+
* restrictions are enforced.
|
|
13
|
+
*/
|
|
14
|
+
export type PrivacyMode = 'strict' | 'loose';
|
|
2
15
|
export default interface BrantaClientOptions {
|
|
3
16
|
baseUrl?: ServerEnvironment | string | null;
|
|
4
17
|
defaultApiKey?: string | null;
|
|
5
18
|
hmacSecret?: string | null;
|
|
6
19
|
timeout?: number;
|
|
20
|
+
/** @see {@link PrivacyMode} */
|
|
21
|
+
privacy: PrivacyMode;
|
|
7
22
|
}
|
package/dist/helpers/aes.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ declare class AesEncryption {
|
|
|
5
5
|
* @param secret - The secret key (will be hashed with SHA-256)
|
|
6
6
|
* @returns Base64-encoded encrypted data (iv + ciphertext + tag)
|
|
7
7
|
*/
|
|
8
|
-
static encrypt(value: string, secret: string): Promise<string>;
|
|
8
|
+
static encrypt(value: string, secret: string, deterministicNonce?: boolean): Promise<string>;
|
|
9
9
|
/**
|
|
10
10
|
* Decrypts an encrypted string using AES-GCM with a secret key
|
|
11
11
|
* @param encryptedValue - Base64-encoded encrypted data
|
package/dist/helpers/aes.js
CHANGED
|
@@ -5,12 +5,20 @@ class AesEncryption {
|
|
|
5
5
|
* @param secret - The secret key (will be hashed with SHA-256)
|
|
6
6
|
* @returns Base64-encoded encrypted data (iv + ciphertext + tag)
|
|
7
7
|
*/
|
|
8
|
-
static async encrypt(value, secret) {
|
|
8
|
+
static async encrypt(value, secret, deterministicNonce = false) {
|
|
9
9
|
try {
|
|
10
10
|
const encoder = new TextEncoder();
|
|
11
11
|
const secretData = encoder.encode(secret);
|
|
12
12
|
const keyData = await crypto.subtle.digest('SHA-256', secretData);
|
|
13
|
-
|
|
13
|
+
let iv;
|
|
14
|
+
if (deterministicNonce) {
|
|
15
|
+
const hmacKey = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
16
|
+
const hmacBuffer = await crypto.subtle.sign('HMAC', hmacKey, encoder.encode(value));
|
|
17
|
+
iv = new Uint8Array(hmacBuffer).slice(0, 12);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
iv = crypto.getRandomValues(new Uint8Array(12));
|
|
21
|
+
}
|
|
14
22
|
const key = await crypto.subtle.importKey('raw', keyData, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
|
|
15
23
|
const plaintext = encoder.encode(value);
|
|
16
24
|
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv, tagLength: 128 }, key, plaintext);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function getHashZkType(value) {
|
|
2
|
+
const lower = value.toLowerCase();
|
|
3
|
+
if (lower.startsWith('lnbc') || lower.startsWith('lntb') || lower.startsWith('lnbcrt'))
|
|
4
|
+
return 'bolt11';
|
|
5
|
+
if (lower.startsWith('ark1'))
|
|
6
|
+
return 'ark_address';
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
export async function toNormalizedHash(value) {
|
|
10
|
+
const normalized = value.toLowerCase();
|
|
11
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(normalized));
|
|
12
|
+
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
13
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import BrantaServerBaseUrl from "./classes/brantaServerBaseUrl.js";
|
|
5
|
-
export { V2BrantaClient, BrantaClientOptions, BrantaServerBaseUrl, Payment, Destination, DestinationType };
|
|
6
|
-
export default V2BrantaClient;
|
|
1
|
+
export { default as BrantaClientOptions } from "./classes/brantaClientOptions.js";
|
|
2
|
+
export { default as BrantaServerBaseUrl } from "./classes/brantaServerBaseUrl.js";
|
|
3
|
+
export type { PrivacyMode } from "./classes/brantaClientOptions.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import BrantaServerBaseUrl from "./classes/brantaServerBaseUrl.js";
|
|
3
|
-
export { V2BrantaClient, BrantaServerBaseUrl };
|
|
4
|
-
export default V2BrantaClient;
|
|
1
|
+
export { default as BrantaServerBaseUrl } from "./classes/brantaServerBaseUrl.js";
|
package/dist/v2/client.d.ts
CHANGED
|
@@ -1,44 +1,14 @@
|
|
|
1
1
|
import BrantaClientOptions from "../classes/brantaClientOptions.js";
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
value: string;
|
|
5
|
-
type?: DestinationType;
|
|
6
|
-
zk?: boolean;
|
|
7
|
-
}
|
|
8
|
-
export interface Payment {
|
|
9
|
-
destinations: Destination[];
|
|
10
|
-
ttl?: number;
|
|
11
|
-
description?: string;
|
|
12
|
-
metadata?: Record<string, string>;
|
|
13
|
-
verifyUrl?: string;
|
|
14
|
-
platformLogoUrl?: string;
|
|
15
|
-
platformLogoLightUrl?: string;
|
|
16
|
-
}
|
|
17
|
-
interface PaymentResponse extends Payment {
|
|
18
|
-
createdAt: Date;
|
|
19
|
-
platform: string;
|
|
20
|
-
}
|
|
21
|
-
interface PaymentResult {
|
|
22
|
-
payment: PaymentResponse;
|
|
23
|
-
verifyLink: string;
|
|
24
|
-
}
|
|
25
|
-
interface ZKPaymentResult extends PaymentResult {
|
|
26
|
-
secret: string;
|
|
27
|
-
}
|
|
28
|
-
export declare class V2BrantaClient {
|
|
2
|
+
import { IBrantaClient, Payment } from "./types.js";
|
|
3
|
+
export declare class BrantaClient implements IBrantaClient {
|
|
29
4
|
private _defaultOptions;
|
|
30
5
|
constructor(brantaClientOptions: BrantaClientOptions);
|
|
31
6
|
getPayments(address: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
32
|
-
|
|
33
|
-
addPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<PaymentResult>;
|
|
34
|
-
addZKPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<ZKPaymentResult>;
|
|
35
|
-
getPaymentsByQRCode(qrText: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
7
|
+
postPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<Payment>;
|
|
36
8
|
isApiKeyValid(options?: BrantaClientOptions | null): Promise<boolean>;
|
|
37
|
-
private _buildVerifyUrl;
|
|
38
9
|
private _resolveBaseUrl;
|
|
39
|
-
private _normalizeAddress;
|
|
40
10
|
private _createClient;
|
|
41
11
|
private _setApiKey;
|
|
42
12
|
private _setHmacHeaders;
|
|
43
13
|
}
|
|
44
|
-
export default
|
|
14
|
+
export default BrantaClient;
|
package/dist/v2/client.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import AesEncryption from "../helpers/aes.js";
|
|
2
1
|
import BrantaPaymentException from "../classes/brantaPaymentException.js";
|
|
3
|
-
export class
|
|
2
|
+
export class BrantaClient {
|
|
4
3
|
constructor(brantaClientOptions) {
|
|
5
4
|
this._defaultOptions = brantaClientOptions;
|
|
6
5
|
}
|
|
@@ -11,16 +10,17 @@ export class V2BrantaClient {
|
|
|
11
10
|
return [];
|
|
12
11
|
}
|
|
13
12
|
const raw = await response.json();
|
|
14
|
-
const data = raw.map(({ platform_logo_url: platformLogoUrl, platform_logo_light_url: platformLogoLightUrl, verify_url: verifyUrl, ...rest }) => ({
|
|
13
|
+
const data = raw.map(({ platform_logo_url: platformLogoUrl, platform_logo_light_url: platformLogoLightUrl, verify_url: verifyUrl, created_at: createdAt, destinations: rawDests, ...rest }) => ({
|
|
15
14
|
...rest,
|
|
16
15
|
platformLogoUrl,
|
|
17
16
|
platformLogoLightUrl,
|
|
18
17
|
verifyUrl,
|
|
18
|
+
createdAt,
|
|
19
|
+
destinations: (rawDests ?? []).map(({ zk_id: zkId, primary: isPrimary, type, ...d }) => ({ ...d, type: type, zkId, isPrimary })),
|
|
19
20
|
}));
|
|
20
21
|
const baseUrl = this._resolveBaseUrl(options);
|
|
21
22
|
const baseOrigin = new URL(baseUrl).origin;
|
|
22
23
|
for (const payment of data) {
|
|
23
|
-
payment.verifyUrl = this._buildVerifyUrl(baseUrl, address);
|
|
24
24
|
if (payment.platformLogoUrl) {
|
|
25
25
|
let valid = false;
|
|
26
26
|
try {
|
|
@@ -42,22 +42,7 @@ export class V2BrantaClient {
|
|
|
42
42
|
}
|
|
43
43
|
return data;
|
|
44
44
|
}
|
|
45
|
-
async
|
|
46
|
-
const payments = await this.getPayments(address, options);
|
|
47
|
-
for (const payment of payments) {
|
|
48
|
-
for (const destination of payment?.destinations || []) {
|
|
49
|
-
if (destination.zk === false)
|
|
50
|
-
continue;
|
|
51
|
-
destination.value = await AesEncryption.decrypt(destination.value, secret);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
const baseUrl = this._resolveBaseUrl(options);
|
|
55
|
-
for (const payment of payments) {
|
|
56
|
-
payment.verifyUrl = this._buildVerifyUrl(baseUrl, address, secret);
|
|
57
|
-
}
|
|
58
|
-
return payments;
|
|
59
|
-
}
|
|
60
|
-
async addPayment(payment, options = null) {
|
|
45
|
+
async postPayment(payment, options = null) {
|
|
61
46
|
const httpClient = this._createClient(options);
|
|
62
47
|
this._setApiKey(httpClient, options);
|
|
63
48
|
await this._setHmacHeaders(httpClient, "POST", "/v2/payments", payment, options);
|
|
@@ -66,63 +51,7 @@ export class V2BrantaClient {
|
|
|
66
51
|
throw new BrantaPaymentException(response.status.toString());
|
|
67
52
|
}
|
|
68
53
|
const responseBody = await response.text();
|
|
69
|
-
|
|
70
|
-
paymentResponse.verifyUrl = this._buildVerifyUrl(httpClient.baseURL, payment.destinations[0].value);
|
|
71
|
-
const verifyLink = httpClient.baseURL + "/v2/verify/" + encodeURIComponent(payment.destinations[0].value);
|
|
72
|
-
return { payment: paymentResponse, verifyLink };
|
|
73
|
-
}
|
|
74
|
-
async addZKPayment(payment, options = null) {
|
|
75
|
-
const secret = crypto.randomUUID();
|
|
76
|
-
for (const destination of payment?.destinations || []) {
|
|
77
|
-
if (destination.zk === false)
|
|
78
|
-
continue;
|
|
79
|
-
destination.value = await AesEncryption.encrypt(destination.value, secret);
|
|
80
|
-
}
|
|
81
|
-
const responsePayment = (await this.addPayment(payment, options));
|
|
82
|
-
responsePayment.secret = secret;
|
|
83
|
-
responsePayment.verifyLink = responsePayment.verifyLink.replace('verify', 'zk-verify') + "#secret=" + secret;
|
|
84
|
-
responsePayment.payment.verifyUrl = this._buildVerifyUrl(this._resolveBaseUrl(options), payment.destinations[0].value, secret);
|
|
85
|
-
return responsePayment;
|
|
86
|
-
}
|
|
87
|
-
async getPaymentsByQRCode(qrText, options = null) {
|
|
88
|
-
const text = qrText.trim();
|
|
89
|
-
let url = null;
|
|
90
|
-
try {
|
|
91
|
-
url = new URL(text);
|
|
92
|
-
}
|
|
93
|
-
catch { /* not a URL */ }
|
|
94
|
-
if (url) {
|
|
95
|
-
const rawParams = new URLSearchParams(url.search.replace(/\+/g, '%2B'));
|
|
96
|
-
const brantaId = rawParams.get('branta_id');
|
|
97
|
-
const brantaSecret = rawParams.get('branta_secret');
|
|
98
|
-
if (brantaId && brantaSecret) {
|
|
99
|
-
return this.getZKPayment(brantaId, brantaSecret, options);
|
|
100
|
-
}
|
|
101
|
-
if (url.protocol === 'bitcoin:') {
|
|
102
|
-
return this.getPayments(this._normalizeAddress(url.pathname), options);
|
|
103
|
-
}
|
|
104
|
-
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
105
|
-
const baseUrl = this._resolveBaseUrl(options);
|
|
106
|
-
if (baseUrl && new URL(baseUrl).origin === url.origin) {
|
|
107
|
-
const segments = url.pathname.split('/').filter(Boolean).map(decodeURIComponent);
|
|
108
|
-
const [version, type, id] = segments;
|
|
109
|
-
if (version === 'v2' && id) {
|
|
110
|
-
if (type === 'verify')
|
|
111
|
-
return this.getPayments(id, options);
|
|
112
|
-
if (type === 'zk-verify') {
|
|
113
|
-
const secret = new URLSearchParams(url.hash.slice(1)).get('secret');
|
|
114
|
-
return secret
|
|
115
|
-
? this.getZKPayment(id, secret, options)
|
|
116
|
-
: this.getPayments(id, options);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const lastSegment = segments.at(-1);
|
|
120
|
-
if (lastSegment)
|
|
121
|
-
return this.getPayments(lastSegment, options);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return this.getPayments(this._normalizeAddress(text), options);
|
|
54
|
+
return JSON.parse(responseBody);
|
|
126
55
|
}
|
|
127
56
|
async isApiKeyValid(options = null) {
|
|
128
57
|
const httpClient = this._createClient(options);
|
|
@@ -130,30 +59,10 @@ export class V2BrantaClient {
|
|
|
130
59
|
const response = await httpClient.get("/v2/api-keys/health-check");
|
|
131
60
|
return response.ok;
|
|
132
61
|
}
|
|
133
|
-
_buildVerifyUrl(baseUrl, address, secret) {
|
|
134
|
-
const encoded = encodeURIComponent(address);
|
|
135
|
-
if (secret) {
|
|
136
|
-
return `${baseUrl}/v2/zk-verify/${encoded}#secret=${secret}`;
|
|
137
|
-
}
|
|
138
|
-
return `${baseUrl}/v2/verify/${encoded}`;
|
|
139
|
-
}
|
|
140
62
|
_resolveBaseUrl(options) {
|
|
141
63
|
const baseUrl = options?.baseUrl ?? this._defaultOptions?.baseUrl;
|
|
142
64
|
return typeof baseUrl === 'string' ? baseUrl : baseUrl?.url ?? '';
|
|
143
65
|
}
|
|
144
|
-
_normalizeAddress(text) {
|
|
145
|
-
const lower = text.toLowerCase();
|
|
146
|
-
if (lower.startsWith('lightning:'))
|
|
147
|
-
return lower.slice('lightning:'.length);
|
|
148
|
-
if (lower.startsWith('bitcoin:')) {
|
|
149
|
-
const addr = text.slice('bitcoin:'.length);
|
|
150
|
-
const addrLower = addr.toLowerCase();
|
|
151
|
-
return addrLower.startsWith('bc1q') || addrLower.startsWith('bcrt') ? addrLower : addr;
|
|
152
|
-
}
|
|
153
|
-
if (lower.startsWith('lnbc') || lower.startsWith('bc1q'))
|
|
154
|
-
return lower;
|
|
155
|
-
return text;
|
|
156
|
-
}
|
|
157
66
|
_createClient(options) {
|
|
158
67
|
const baseUrl = options?.baseUrl ?? this._defaultOptions?.baseUrl;
|
|
159
68
|
const timeout = options?.timeout ?? this._defaultOptions?.timeout ?? 10000;
|
|
@@ -249,4 +158,4 @@ export class V2BrantaClient {
|
|
|
249
158
|
};
|
|
250
159
|
}
|
|
251
160
|
}
|
|
252
|
-
export default
|
|
161
|
+
export default BrantaClient;
|
package/dist/v2/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BrantaClientOptions from "../classes/brantaClientOptions.js";
|
|
2
|
+
import { IBrantaClient, IBrantaService, Payment, ZKPaymentResult } from "./types.js";
|
|
3
|
+
export declare class BrantaService implements IBrantaService {
|
|
4
|
+
private readonly _client;
|
|
5
|
+
private readonly _defaultOptions;
|
|
6
|
+
constructor(defaultOptions: BrantaClientOptions, client?: IBrantaClient);
|
|
7
|
+
getPayments(address: string, destinationEncryptionKey?: string | null, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
8
|
+
addPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<ZKPaymentResult>;
|
|
9
|
+
getPaymentsByQRCode(qrText: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
10
|
+
isApiKeyValid(options?: BrantaClientOptions | null): Promise<boolean>;
|
|
11
|
+
private _getPaymentsForZk;
|
|
12
|
+
private _decryptDestinations;
|
|
13
|
+
private _decryptHashZkDestinations;
|
|
14
|
+
private _buildVerifyUrl;
|
|
15
|
+
private _getPlainPayments;
|
|
16
|
+
private _resolveBaseUrl;
|
|
17
|
+
private _normalizeAddress;
|
|
18
|
+
}
|
|
19
|
+
export default BrantaService;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import AesEncryption from "../helpers/aes.js";
|
|
2
|
+
import { getHashZkType, toNormalizedHash } from "../helpers/hashZk.js";
|
|
3
|
+
import BrantaPaymentException from "../classes/brantaPaymentException.js";
|
|
4
|
+
import { BrantaClient } from "./client.js";
|
|
5
|
+
export class BrantaService {
|
|
6
|
+
constructor(defaultOptions, client) {
|
|
7
|
+
this._defaultOptions = defaultOptions;
|
|
8
|
+
this._client = client ?? new BrantaClient(defaultOptions);
|
|
9
|
+
}
|
|
10
|
+
async getPayments(address, destinationEncryptionKey = null, options = null) {
|
|
11
|
+
const hashZkType = getHashZkType(address);
|
|
12
|
+
if (!hashZkType && !destinationEncryptionKey) {
|
|
13
|
+
const privacy = options?.privacy ?? this._defaultOptions?.privacy;
|
|
14
|
+
if (privacy === 'strict') {
|
|
15
|
+
throw new BrantaPaymentException("privacy is set to 'strict': plain on-chain address lookups are not permitted");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
let lookupValue = address;
|
|
19
|
+
if (hashZkType) {
|
|
20
|
+
const hash = await toNormalizedHash(address);
|
|
21
|
+
lookupValue = await AesEncryption.encrypt(address, hash, true);
|
|
22
|
+
}
|
|
23
|
+
let payments = await this._client.getPayments(lookupValue, options);
|
|
24
|
+
if (payments.length === 0 && hashZkType) {
|
|
25
|
+
const privacy = options?.privacy ?? this._defaultOptions?.privacy;
|
|
26
|
+
if (privacy !== 'strict') {
|
|
27
|
+
lookupValue = address;
|
|
28
|
+
payments = await this._client.getPayments(address, options);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const baseUrl = this._resolveBaseUrl(options);
|
|
32
|
+
for (const payment of payments) {
|
|
33
|
+
const keys = await this._decryptDestinations(payment.destinations, address, destinationEncryptionKey, hashZkType);
|
|
34
|
+
payment.verifyUrl = this._buildVerifyUrl(baseUrl, lookupValue, keys);
|
|
35
|
+
}
|
|
36
|
+
return payments;
|
|
37
|
+
}
|
|
38
|
+
async addPayment(payment, options = null) {
|
|
39
|
+
const privacy = options?.privacy ?? this._defaultOptions?.privacy;
|
|
40
|
+
if (privacy === 'strict' && payment.destinations.some(d => !d.zk)) {
|
|
41
|
+
throw new BrantaPaymentException("privacy is set to 'strict': all destinations must have zk enabled");
|
|
42
|
+
}
|
|
43
|
+
const secret = crypto.randomUUID();
|
|
44
|
+
const encryptedToKey = new Map();
|
|
45
|
+
for (const dest of payment.destinations) {
|
|
46
|
+
if (!dest.zk)
|
|
47
|
+
continue;
|
|
48
|
+
if (dest.type === 'bitcoin_address') {
|
|
49
|
+
dest.value = await AesEncryption.encrypt(dest.value, secret, false);
|
|
50
|
+
encryptedToKey.set(dest.value, secret);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const hashZkType = getHashZkType(dest.value);
|
|
54
|
+
if (!hashZkType) {
|
|
55
|
+
throw new BrantaPaymentException(`destination type '${dest.type}' does not support ZK`);
|
|
56
|
+
}
|
|
57
|
+
const hash = await toNormalizedHash(dest.value);
|
|
58
|
+
dest.value = await AesEncryption.encrypt(dest.value, hash, true);
|
|
59
|
+
encryptedToKey.set(dest.value, hash);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const responsePayment = await this._client.postPayment(payment, options);
|
|
63
|
+
const keys = new Map();
|
|
64
|
+
for (const dest of responsePayment.destinations) {
|
|
65
|
+
if (dest.zkId && encryptedToKey.has(dest.value)) {
|
|
66
|
+
keys.set(dest.zkId, encryptedToKey.get(dest.value));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const baseUrl = this._resolveBaseUrl(options);
|
|
70
|
+
const primaryValue = payment.destinations[0]?.value ?? '';
|
|
71
|
+
responsePayment.verifyUrl = this._buildVerifyUrl(baseUrl, primaryValue, keys);
|
|
72
|
+
const verifyLink = responsePayment.verifyUrl;
|
|
73
|
+
return { payment: responsePayment, verifyLink, secret };
|
|
74
|
+
}
|
|
75
|
+
async getPaymentsByQRCode(qrText, options = null) {
|
|
76
|
+
const text = qrText.trim();
|
|
77
|
+
let url = null;
|
|
78
|
+
try {
|
|
79
|
+
url = new URL(text);
|
|
80
|
+
}
|
|
81
|
+
catch { /* not a URL */ }
|
|
82
|
+
if (!url)
|
|
83
|
+
return this._getPlainPayments(this._normalizeAddress(text), options);
|
|
84
|
+
if (url.protocol === 'bitcoin:' || url.protocol === 'lightning:') {
|
|
85
|
+
const brantaId = url.searchParams.get('branta_id');
|
|
86
|
+
const brantaSecret = url.searchParams.get('branta_secret');
|
|
87
|
+
if (brantaId && brantaSecret) {
|
|
88
|
+
const additionalValues = [];
|
|
89
|
+
const lightning = url.searchParams.get('lightning');
|
|
90
|
+
const bolt12 = url.searchParams.get('bolt12');
|
|
91
|
+
const ark = url.searchParams.get('ark');
|
|
92
|
+
if (lightning)
|
|
93
|
+
additionalValues.push(lightning);
|
|
94
|
+
if (bolt12)
|
|
95
|
+
additionalValues.push(bolt12);
|
|
96
|
+
if (ark)
|
|
97
|
+
additionalValues.push(ark);
|
|
98
|
+
return this._getPaymentsForZk(brantaId, brantaSecret, additionalValues, options);
|
|
99
|
+
}
|
|
100
|
+
return this._getPlainPayments(this._normalizeAddress(url.pathname), options);
|
|
101
|
+
}
|
|
102
|
+
const brantaId = url.searchParams.get('branta_id');
|
|
103
|
+
const brantaSecret = url.searchParams.get('branta_secret');
|
|
104
|
+
if (brantaId && brantaSecret)
|
|
105
|
+
return this.getPayments(brantaId, brantaSecret, options);
|
|
106
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
107
|
+
const baseUrl = this._resolveBaseUrl(options);
|
|
108
|
+
if (!baseUrl || new URL(baseUrl).origin !== url.origin) {
|
|
109
|
+
return this._getPlainPayments(this._normalizeAddress(text), options);
|
|
110
|
+
}
|
|
111
|
+
const segments = url.pathname.split('/').filter(Boolean).map(decodeURIComponent);
|
|
112
|
+
const [version, type, id] = segments;
|
|
113
|
+
if (version === 'v2' && id) {
|
|
114
|
+
if (type === 'verify')
|
|
115
|
+
return this._getPlainPayments(id, options);
|
|
116
|
+
if (type === 'zk-verify') {
|
|
117
|
+
const secret = new URLSearchParams(url.hash.slice(1)).get('secret');
|
|
118
|
+
if (secret)
|
|
119
|
+
return this.getPayments(id, secret, options);
|
|
120
|
+
return this._getPlainPayments(id, options);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const lastSegment = segments.at(-1);
|
|
124
|
+
if (lastSegment)
|
|
125
|
+
return this._getPlainPayments(lastSegment, options);
|
|
126
|
+
}
|
|
127
|
+
return this._getPlainPayments(this._normalizeAddress(text), options);
|
|
128
|
+
}
|
|
129
|
+
async isApiKeyValid(options = null) {
|
|
130
|
+
return this._client.isApiKeyValid(options);
|
|
131
|
+
}
|
|
132
|
+
async _getPaymentsForZk(lookupValue, encryptionKey, additionalHashValues, options) {
|
|
133
|
+
const payments = await this._client.getPayments(lookupValue, options);
|
|
134
|
+
const baseUrl = this._resolveBaseUrl(options);
|
|
135
|
+
for (const payment of payments) {
|
|
136
|
+
const keys = await this._decryptDestinations(payment.destinations, lookupValue, encryptionKey, null);
|
|
137
|
+
for (const value of additionalHashValues) {
|
|
138
|
+
await this._decryptHashZkDestinations(payment.destinations, value, keys);
|
|
139
|
+
}
|
|
140
|
+
payment.verifyUrl = this._buildVerifyUrl(baseUrl, lookupValue, keys);
|
|
141
|
+
}
|
|
142
|
+
return payments;
|
|
143
|
+
}
|
|
144
|
+
async _decryptDestinations(destinations, destinationValue, encryptionKey, hashZkType) {
|
|
145
|
+
const keys = new Map();
|
|
146
|
+
for (const dest of destinations) {
|
|
147
|
+
if (!dest.zk)
|
|
148
|
+
continue;
|
|
149
|
+
if (dest.type === 'bitcoin_address') {
|
|
150
|
+
if (!encryptionKey)
|
|
151
|
+
throw new BrantaPaymentException("Payment is ZK but no destination encryption key was provided.");
|
|
152
|
+
dest.value = await AesEncryption.decrypt(dest.value, encryptionKey);
|
|
153
|
+
if (dest.zkId)
|
|
154
|
+
keys.set(dest.zkId, encryptionKey);
|
|
155
|
+
}
|
|
156
|
+
else if (hashZkType && dest.type === hashZkType) {
|
|
157
|
+
const hash = await toNormalizedHash(destinationValue);
|
|
158
|
+
dest.value = await AesEncryption.decrypt(dest.value, hash);
|
|
159
|
+
if (dest.zkId)
|
|
160
|
+
keys.set(dest.zkId, hash);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return keys;
|
|
164
|
+
}
|
|
165
|
+
async _decryptHashZkDestinations(destinations, plainValue, keys) {
|
|
166
|
+
const hashZkType = getHashZkType(plainValue);
|
|
167
|
+
if (!hashZkType)
|
|
168
|
+
return;
|
|
169
|
+
const hash = await toNormalizedHash(plainValue);
|
|
170
|
+
for (const dest of destinations) {
|
|
171
|
+
if (!dest.zk || dest.type !== hashZkType)
|
|
172
|
+
continue;
|
|
173
|
+
dest.value = await AesEncryption.decrypt(dest.value, hash);
|
|
174
|
+
if (dest.zkId)
|
|
175
|
+
keys.set(dest.zkId, hash);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
_buildVerifyUrl(baseUrl, lookupValue, keys) {
|
|
179
|
+
const encoded = encodeURIComponent(lookupValue);
|
|
180
|
+
let url = `${baseUrl}/v2/verify/${encoded}`;
|
|
181
|
+
if (keys.size > 0) {
|
|
182
|
+
const fragment = Array.from(keys.entries()).map(([id, key]) => `k-${id}=${key}`).join('&');
|
|
183
|
+
url += `#${fragment}`;
|
|
184
|
+
}
|
|
185
|
+
return url;
|
|
186
|
+
}
|
|
187
|
+
_getPlainPayments(address, options) {
|
|
188
|
+
const privacy = options?.privacy ?? this._defaultOptions?.privacy;
|
|
189
|
+
if (privacy === 'strict' && !getHashZkType(address))
|
|
190
|
+
return Promise.resolve([]);
|
|
191
|
+
return this.getPayments(address, null, options);
|
|
192
|
+
}
|
|
193
|
+
_resolveBaseUrl(options) {
|
|
194
|
+
const baseUrl = options?.baseUrl ?? this._defaultOptions?.baseUrl;
|
|
195
|
+
return typeof baseUrl === 'string' ? baseUrl : baseUrl?.url ?? '';
|
|
196
|
+
}
|
|
197
|
+
_normalizeAddress(text) {
|
|
198
|
+
const lower = text.toLowerCase();
|
|
199
|
+
if (lower.startsWith('lightning:'))
|
|
200
|
+
return lower.slice('lightning:'.length);
|
|
201
|
+
if (lower.startsWith('bitcoin:')) {
|
|
202
|
+
const addr = text.slice('bitcoin:'.length);
|
|
203
|
+
const addrLower = addr.toLowerCase();
|
|
204
|
+
return addrLower.startsWith('bc1q') || addrLower.startsWith('bcrt') ? addrLower : addr;
|
|
205
|
+
}
|
|
206
|
+
if (lower.startsWith('lnbc') || lower.startsWith('lntb') || lower.startsWith('lnbcrt') || lower.startsWith('bc1q'))
|
|
207
|
+
return lower;
|
|
208
|
+
return text;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export default BrantaService;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import BrantaClientOptions from "../classes/brantaClientOptions.js";
|
|
2
|
+
export type DestinationType = 'bitcoin_address' | 'ln_address' | 'bolt11' | 'bolt12' | 'ln_url' | 'tether_address' | 'ark_address';
|
|
3
|
+
export interface Destination {
|
|
4
|
+
value: string;
|
|
5
|
+
type?: DestinationType;
|
|
6
|
+
zk?: boolean;
|
|
7
|
+
zkId?: string;
|
|
8
|
+
isPrimary?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface Payment {
|
|
11
|
+
destinations: Destination[];
|
|
12
|
+
ttl?: number;
|
|
13
|
+
description?: string;
|
|
14
|
+
metadata?: Record<string, string>;
|
|
15
|
+
verifyUrl?: string;
|
|
16
|
+
platform?: string;
|
|
17
|
+
platformLogoUrl?: string;
|
|
18
|
+
platformLogoLightUrl?: string;
|
|
19
|
+
createdAt?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PaymentResult {
|
|
22
|
+
payment: Payment;
|
|
23
|
+
verifyLink: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ZKPaymentResult extends PaymentResult {
|
|
26
|
+
secret: string;
|
|
27
|
+
}
|
|
28
|
+
export interface IBrantaClient {
|
|
29
|
+
getPayments(address: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
30
|
+
postPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<Payment>;
|
|
31
|
+
isApiKeyValid(options?: BrantaClientOptions | null): Promise<boolean>;
|
|
32
|
+
}
|
|
33
|
+
export interface IBrantaService {
|
|
34
|
+
getPayments(address: string, destinationEncryptionKey?: string | null, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
35
|
+
addPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<ZKPaymentResult>;
|
|
36
|
+
getPaymentsByQRCode(qrText: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
37
|
+
isApiKeyValid(options?: BrantaClientOptions | null): Promise<boolean>;
|
|
38
|
+
}
|
package/dist/v2/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@branta-ops/branta",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "A JavaScript SDK for the Branta API",
|
|
5
5
|
"homepage": "https://github.com/BrantaOps/branta-js#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./v2": {
|
|
24
|
+
"import": "./dist/v2/index.js",
|
|
25
|
+
"types": "./dist/v2/index.d.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
18
28
|
"files": [
|
|
19
29
|
"dist"
|
|
20
30
|
],
|