@branta-ops/branta 2.0.0 → 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/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 -37
- package/dist/v2/client.js +7 -111
- 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/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,47 +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
|
-
platform?: string;
|
|
15
|
-
platformLogoUrl?: string;
|
|
16
|
-
platformLogoLightUrl?: string;
|
|
17
|
-
}
|
|
18
|
-
interface PaymentResponse extends Payment {
|
|
19
|
-
createdAt: Date;
|
|
20
|
-
platform: string;
|
|
21
|
-
}
|
|
22
|
-
interface PaymentResult {
|
|
23
|
-
payment: PaymentResponse;
|
|
24
|
-
verifyLink: string;
|
|
25
|
-
}
|
|
26
|
-
interface ZKPaymentResult extends PaymentResult {
|
|
27
|
-
secret: string;
|
|
28
|
-
}
|
|
29
|
-
export declare class V2BrantaClient {
|
|
2
|
+
import { IBrantaClient, Payment } from "./types.js";
|
|
3
|
+
export declare class BrantaClient implements IBrantaClient {
|
|
30
4
|
private _defaultOptions;
|
|
31
5
|
constructor(brantaClientOptions: BrantaClientOptions);
|
|
32
6
|
getPayments(address: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
33
|
-
|
|
34
|
-
getZKPayment(address: string, secret: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
35
|
-
addPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<PaymentResult>;
|
|
36
|
-
addZKPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<ZKPaymentResult>;
|
|
37
|
-
getPaymentsByQRCode(qrText: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
38
|
-
private _getPlainPayments;
|
|
7
|
+
postPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<Payment>;
|
|
39
8
|
isApiKeyValid(options?: BrantaClientOptions | null): Promise<boolean>;
|
|
40
|
-
private _buildVerifyUrl;
|
|
41
9
|
private _resolveBaseUrl;
|
|
42
|
-
private _normalizeAddress;
|
|
43
10
|
private _createClient;
|
|
44
11
|
private _setApiKey;
|
|
45
12
|
private _setHmacHeaders;
|
|
46
13
|
}
|
|
47
|
-
export default
|
|
14
|
+
export default BrantaClient;
|
package/dist/v2/client.js
CHANGED
|
@@ -1,33 +1,26 @@
|
|
|
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
|
}
|
|
7
6
|
async getPayments(address, options = null) {
|
|
8
|
-
const privacy = options?.privacy ?? this._defaultOptions?.privacy;
|
|
9
|
-
if (privacy === 'strict') {
|
|
10
|
-
throw new BrantaPaymentException("privacy is set to 'strict': plain on-chain address lookups are not permitted");
|
|
11
|
-
}
|
|
12
|
-
return this._fetchPayments(address, options);
|
|
13
|
-
}
|
|
14
|
-
async _fetchPayments(address, options = null) {
|
|
15
7
|
const httpClient = this._createClient(options);
|
|
16
8
|
const response = await httpClient.get(`/v2/payments/${encodeURIComponent(address)}`);
|
|
17
9
|
if (!response.ok || response.headers.get("content-length") === "0") {
|
|
18
10
|
return [];
|
|
19
11
|
}
|
|
20
12
|
const raw = await response.json();
|
|
21
|
-
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 }) => ({
|
|
22
14
|
...rest,
|
|
23
15
|
platformLogoUrl,
|
|
24
16
|
platformLogoLightUrl,
|
|
25
17
|
verifyUrl,
|
|
18
|
+
createdAt,
|
|
19
|
+
destinations: (rawDests ?? []).map(({ zk_id: zkId, primary: isPrimary, type, ...d }) => ({ ...d, type: type, zkId, isPrimary })),
|
|
26
20
|
}));
|
|
27
21
|
const baseUrl = this._resolveBaseUrl(options);
|
|
28
22
|
const baseOrigin = new URL(baseUrl).origin;
|
|
29
23
|
for (const payment of data) {
|
|
30
|
-
payment.verifyUrl = this._buildVerifyUrl(baseUrl, address);
|
|
31
24
|
if (payment.platformLogoUrl) {
|
|
32
25
|
let valid = false;
|
|
33
26
|
try {
|
|
@@ -49,22 +42,7 @@ export class V2BrantaClient {
|
|
|
49
42
|
}
|
|
50
43
|
return data;
|
|
51
44
|
}
|
|
52
|
-
async
|
|
53
|
-
const payments = await this._fetchPayments(address, options);
|
|
54
|
-
for (const payment of payments) {
|
|
55
|
-
for (const destination of payment?.destinations || []) {
|
|
56
|
-
if (destination.zk === false)
|
|
57
|
-
continue;
|
|
58
|
-
destination.value = await AesEncryption.decrypt(destination.value, secret);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const baseUrl = this._resolveBaseUrl(options);
|
|
62
|
-
for (const payment of payments) {
|
|
63
|
-
payment.verifyUrl = this._buildVerifyUrl(baseUrl, address, secret);
|
|
64
|
-
}
|
|
65
|
-
return payments;
|
|
66
|
-
}
|
|
67
|
-
async addPayment(payment, options = null) {
|
|
45
|
+
async postPayment(payment, options = null) {
|
|
68
46
|
const httpClient = this._createClient(options);
|
|
69
47
|
this._setApiKey(httpClient, options);
|
|
70
48
|
await this._setHmacHeaders(httpClient, "POST", "/v2/payments", payment, options);
|
|
@@ -73,69 +51,7 @@ export class V2BrantaClient {
|
|
|
73
51
|
throw new BrantaPaymentException(response.status.toString());
|
|
74
52
|
}
|
|
75
53
|
const responseBody = await response.text();
|
|
76
|
-
|
|
77
|
-
paymentResponse.verifyUrl = this._buildVerifyUrl(httpClient.baseURL, payment.destinations[0].value);
|
|
78
|
-
const verifyLink = httpClient.baseURL + "/v2/verify/" + encodeURIComponent(payment.destinations[0].value);
|
|
79
|
-
return { payment: paymentResponse, verifyLink };
|
|
80
|
-
}
|
|
81
|
-
async addZKPayment(payment, options = null) {
|
|
82
|
-
const secret = crypto.randomUUID();
|
|
83
|
-
for (const destination of payment?.destinations || []) {
|
|
84
|
-
if (destination.zk === false)
|
|
85
|
-
continue;
|
|
86
|
-
destination.value = await AesEncryption.encrypt(destination.value, secret);
|
|
87
|
-
}
|
|
88
|
-
const responsePayment = (await this.addPayment(payment, options));
|
|
89
|
-
responsePayment.secret = secret;
|
|
90
|
-
responsePayment.verifyLink = responsePayment.verifyLink.replace('verify', 'zk-verify') + "#secret=" + secret;
|
|
91
|
-
responsePayment.payment.verifyUrl = this._buildVerifyUrl(this._resolveBaseUrl(options), payment.destinations[0].value, secret);
|
|
92
|
-
return responsePayment;
|
|
93
|
-
}
|
|
94
|
-
async getPaymentsByQRCode(qrText, options = null) {
|
|
95
|
-
const text = qrText.trim();
|
|
96
|
-
let url = null;
|
|
97
|
-
try {
|
|
98
|
-
url = new URL(text);
|
|
99
|
-
}
|
|
100
|
-
catch { /* not a URL */ }
|
|
101
|
-
if (!url)
|
|
102
|
-
return this._getPlainPayments(this._normalizeAddress(text), options);
|
|
103
|
-
const rawParams = new URLSearchParams(url.search.replace(/\+/g, '%2B'));
|
|
104
|
-
const brantaId = rawParams.get('branta_id');
|
|
105
|
-
const brantaSecret = rawParams.get('branta_secret');
|
|
106
|
-
if (brantaId && brantaSecret)
|
|
107
|
-
return this.getZKPayment(brantaId, brantaSecret, options);
|
|
108
|
-
if (url.protocol === 'bitcoin:') {
|
|
109
|
-
return this._getPlainPayments(this._normalizeAddress(url.pathname), options);
|
|
110
|
-
}
|
|
111
|
-
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
112
|
-
const baseUrl = this._resolveBaseUrl(options);
|
|
113
|
-
if (!baseUrl || new URL(baseUrl).origin !== url.origin) {
|
|
114
|
-
return this._getPlainPayments(this._normalizeAddress(text), options);
|
|
115
|
-
}
|
|
116
|
-
const segments = url.pathname.split('/').filter(Boolean).map(decodeURIComponent);
|
|
117
|
-
const [version, type, id] = segments;
|
|
118
|
-
if (version === 'v2' && id) {
|
|
119
|
-
if (type === 'verify')
|
|
120
|
-
return this._getPlainPayments(id, options);
|
|
121
|
-
if (type === 'zk-verify') {
|
|
122
|
-
const secret = new URLSearchParams(url.hash.slice(1)).get('secret');
|
|
123
|
-
if (secret)
|
|
124
|
-
return this.getZKPayment(id, secret, options);
|
|
125
|
-
return this._getPlainPayments(id, options);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const lastSegment = segments.at(-1);
|
|
129
|
-
if (lastSegment)
|
|
130
|
-
return this._getPlainPayments(lastSegment, options);
|
|
131
|
-
}
|
|
132
|
-
return this._getPlainPayments(this._normalizeAddress(text), options);
|
|
133
|
-
}
|
|
134
|
-
_getPlainPayments(address, options) {
|
|
135
|
-
const privacy = options?.privacy ?? this._defaultOptions?.privacy;
|
|
136
|
-
if (privacy === 'strict')
|
|
137
|
-
return Promise.resolve([]);
|
|
138
|
-
return this.getPayments(address, options);
|
|
54
|
+
return JSON.parse(responseBody);
|
|
139
55
|
}
|
|
140
56
|
async isApiKeyValid(options = null) {
|
|
141
57
|
const httpClient = this._createClient(options);
|
|
@@ -143,30 +59,10 @@ export class V2BrantaClient {
|
|
|
143
59
|
const response = await httpClient.get("/v2/api-keys/health-check");
|
|
144
60
|
return response.ok;
|
|
145
61
|
}
|
|
146
|
-
_buildVerifyUrl(baseUrl, address, secret) {
|
|
147
|
-
const encoded = encodeURIComponent(address);
|
|
148
|
-
if (secret) {
|
|
149
|
-
return `${baseUrl}/v2/zk-verify/${encoded}#secret=${secret}`;
|
|
150
|
-
}
|
|
151
|
-
return `${baseUrl}/v2/verify/${encoded}`;
|
|
152
|
-
}
|
|
153
62
|
_resolveBaseUrl(options) {
|
|
154
63
|
const baseUrl = options?.baseUrl ?? this._defaultOptions?.baseUrl;
|
|
155
64
|
return typeof baseUrl === 'string' ? baseUrl : baseUrl?.url ?? '';
|
|
156
65
|
}
|
|
157
|
-
_normalizeAddress(text) {
|
|
158
|
-
const lower = text.toLowerCase();
|
|
159
|
-
if (lower.startsWith('lightning:'))
|
|
160
|
-
return lower.slice('lightning:'.length);
|
|
161
|
-
if (lower.startsWith('bitcoin:')) {
|
|
162
|
-
const addr = text.slice('bitcoin:'.length);
|
|
163
|
-
const addrLower = addr.toLowerCase();
|
|
164
|
-
return addrLower.startsWith('bc1q') || addrLower.startsWith('bcrt') ? addrLower : addr;
|
|
165
|
-
}
|
|
166
|
-
if (lower.startsWith('lnbc') || lower.startsWith('bc1q'))
|
|
167
|
-
return lower;
|
|
168
|
-
return text;
|
|
169
|
-
}
|
|
170
66
|
_createClient(options) {
|
|
171
67
|
const baseUrl = options?.baseUrl ?? this._defaultOptions?.baseUrl;
|
|
172
68
|
const timeout = options?.timeout ?? this._defaultOptions?.timeout ?? 10000;
|
|
@@ -262,4 +158,4 @@ export class V2BrantaClient {
|
|
|
262
158
|
};
|
|
263
159
|
}
|
|
264
160
|
}
|
|
265
|
-
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
|
],
|