@branta-ops/branta 0.0.1 → 0.0.3
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/LICENSE +21 -0
- package/README.md +57 -31
- package/dist/classes/brantaBaseServerUrl.d.ts +11 -0
- package/dist/classes/brantaBaseServerUrl.js +6 -0
- package/dist/classes/brantaClientOptions.d.ts +7 -0
- package/dist/classes/brantaClientOptions.js +1 -0
- package/dist/classes/brantaPaymentException.d.ts +4 -0
- package/dist/classes/brantaPaymentException.js +7 -0
- package/dist/classes/brantaServerBaseUrl.d.ts +16 -0
- package/dist/classes/brantaServerBaseUrl.js +6 -0
- package/dist/helpers/aes.d.ts +17 -0
- package/dist/helpers/aes.js +48 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/v2/client.d.ts +36 -0
- package/dist/v2/client.js +153 -0
- package/package.json +14 -4
- package/jest.config.js +0 -4
- package/src/classes/brantaBaseServerUrl.js +0 -7
- package/src/classes/brantaClientOptions.js +0 -10
- package/src/classes/brantaPaymentException.js +0 -8
- package/src/helpers/aes.js +0 -83
- package/src/index.js +0 -5
- package/src/v2/client.js +0 -125
- package/test/helpers/aes.test.js +0 -35
- package/test/script.js +0 -28
- package/test/v2/client.test.js +0 -310
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Branta Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -7,39 +7,65 @@ Package contains functionality to assist JavaScript projects with making request
|
|
|
7
7
|
Install via npm:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm
|
|
10
|
+
npm i @branta-ops/branta
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
14
|
+
|
|
15
|
+
### For Wallets
|
|
16
|
+
```ts
|
|
17
|
+
import { V2BrantaClient, BrantaServerBaseUrl } from "@branta-ops/branta";
|
|
18
|
+
|
|
19
|
+
const client = new V2BrantaClient({
|
|
20
|
+
baseUrl: BrantaServerBaseUrl.Production,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await client.getPayments("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### For Platforms
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { V2BrantaClient, BrantaServerBaseUrl } from "@branta-ops/branta";
|
|
30
|
+
|
|
31
|
+
const client = new V2BrantaClient({
|
|
32
|
+
baseUrl: BrantaServerBaseUrl.Production,
|
|
33
|
+
defaultApiKey: "<default-api-key>",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await client.addPayment({
|
|
37
|
+
description: "Testing description",
|
|
38
|
+
destinations: [
|
|
39
|
+
{
|
|
40
|
+
value: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
|
|
41
|
+
isZk: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
ttl: 600,
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### For Parent Platforms
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { V2BrantaClient, BrantaServerBaseUrl } from "@branta-ops/branta";
|
|
52
|
+
|
|
53
|
+
const client = new V2BrantaClient({
|
|
54
|
+
baseUrl: BrantaServerBaseUrl.Production,
|
|
55
|
+
defaultApiKey: "<default-api-key>",
|
|
56
|
+
hmacSecret: "<hmac-secret>",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await client.addPayment({
|
|
60
|
+
description: "Testing description",
|
|
61
|
+
destinations: [
|
|
62
|
+
{
|
|
63
|
+
value: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
|
|
64
|
+
isZk: false,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
ttl: 600,
|
|
68
|
+
});
|
|
43
69
|
```
|
|
44
70
|
|
|
45
71
|
## Feature Support
|
|
@@ -49,6 +75,6 @@ if (payments.length == 0) {
|
|
|
49
75
|
- [ ] V2 Get Payment by QR Code
|
|
50
76
|
- [X] V2 Get decrypted Zero Knowledge by address and secret
|
|
51
77
|
- [X] V2 Add Payment
|
|
52
|
-
- [
|
|
78
|
+
- [X] V2 Payment by Parent Platform with HMAC
|
|
53
79
|
- [X] V2 Add Zero Knowledge Payment with secret
|
|
54
80
|
- [X] V2 Check API key valid
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ServerEnvironment {
|
|
2
|
+
value: number;
|
|
3
|
+
url: string;
|
|
4
|
+
}
|
|
5
|
+
export interface BrantaServerBaseUrlType {
|
|
6
|
+
Staging: ServerEnvironment;
|
|
7
|
+
Production: ServerEnvironment;
|
|
8
|
+
Localhost: ServerEnvironment;
|
|
9
|
+
}
|
|
10
|
+
declare const BrantaServerBaseUrl: BrantaServerBaseUrlType;
|
|
11
|
+
export default BrantaServerBaseUrl;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare const BrantaServerBaseUrl: {
|
|
2
|
+
readonly Staging: {
|
|
3
|
+
readonly value: 0;
|
|
4
|
+
readonly url: "https://staging.guardrail.branta.pro";
|
|
5
|
+
};
|
|
6
|
+
readonly Production: {
|
|
7
|
+
readonly value: 1;
|
|
8
|
+
readonly url: "https://guardrail.branta.pro";
|
|
9
|
+
};
|
|
10
|
+
readonly Localhost: {
|
|
11
|
+
readonly value: 2;
|
|
12
|
+
readonly url: "http://localhost:3000";
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type ServerEnvironment = typeof BrantaServerBaseUrl[keyof typeof BrantaServerBaseUrl];
|
|
16
|
+
export default BrantaServerBaseUrl;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare class AesEncryption {
|
|
2
|
+
/**
|
|
3
|
+
* Encrypts a string value using AES-GCM with a secret key
|
|
4
|
+
* @param value - The plaintext to encrypt
|
|
5
|
+
* @param secret - The secret key (will be hashed with SHA-256)
|
|
6
|
+
* @returns Base64-encoded encrypted data (iv + ciphertext + tag)
|
|
7
|
+
*/
|
|
8
|
+
static encrypt(value: string, secret: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Decrypts an encrypted string using AES-GCM with a secret key
|
|
11
|
+
* @param encryptedValue - Base64-encoded encrypted data
|
|
12
|
+
* @param secret - The secret key (will be hashed with SHA-256)
|
|
13
|
+
* @returns The decrypted plaintext
|
|
14
|
+
*/
|
|
15
|
+
static decrypt(encryptedValue: string, secret: string): Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
export default AesEncryption;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class AesEncryption {
|
|
2
|
+
/**
|
|
3
|
+
* Encrypts a string value using AES-GCM with a secret key
|
|
4
|
+
* @param value - The plaintext to encrypt
|
|
5
|
+
* @param secret - The secret key (will be hashed with SHA-256)
|
|
6
|
+
* @returns Base64-encoded encrypted data (iv + ciphertext + tag)
|
|
7
|
+
*/
|
|
8
|
+
static async encrypt(value, secret) {
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const secretData = encoder.encode(secret);
|
|
11
|
+
const keyData = await crypto.subtle.digest('SHA-256', secretData);
|
|
12
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
13
|
+
const key = await crypto.subtle.importKey('raw', keyData, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
|
|
14
|
+
const plaintext = encoder.encode(value);
|
|
15
|
+
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv, tagLength: 128 }, key, plaintext);
|
|
16
|
+
const encryptedArray = new Uint8Array(encrypted);
|
|
17
|
+
const ciphertext = encryptedArray.slice(0, -16);
|
|
18
|
+
const tag = encryptedArray.slice(-16);
|
|
19
|
+
const result = new Uint8Array(iv.length + ciphertext.length + tag.length);
|
|
20
|
+
result.set(iv, 0);
|
|
21
|
+
result.set(ciphertext, iv.length);
|
|
22
|
+
result.set(tag, iv.length + ciphertext.length);
|
|
23
|
+
return btoa(String.fromCharCode(...result));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decrypts an encrypted string using AES-GCM with a secret key
|
|
27
|
+
* @param encryptedValue - Base64-encoded encrypted data
|
|
28
|
+
* @param secret - The secret key (will be hashed with SHA-256)
|
|
29
|
+
* @returns The decrypted plaintext
|
|
30
|
+
*/
|
|
31
|
+
static async decrypt(encryptedValue, secret) {
|
|
32
|
+
const encryptedData = Uint8Array.from(atob(encryptedValue), c => c.charCodeAt(0));
|
|
33
|
+
const encoder = new TextEncoder();
|
|
34
|
+
const secretData = encoder.encode(secret);
|
|
35
|
+
const keyData = await crypto.subtle.digest('SHA-256', secretData);
|
|
36
|
+
const iv = encryptedData.slice(0, 12);
|
|
37
|
+
const tag = encryptedData.slice(-16);
|
|
38
|
+
const ciphertext = encryptedData.slice(12, -16);
|
|
39
|
+
const ciphertextWithTag = new Uint8Array(ciphertext.length + tag.length);
|
|
40
|
+
ciphertextWithTag.set(ciphertext, 0);
|
|
41
|
+
ciphertextWithTag.set(tag, ciphertext.length);
|
|
42
|
+
const key = await crypto.subtle.importKey('raw', keyData, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
|
|
43
|
+
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv, tagLength: 128 }, key, ciphertextWithTag);
|
|
44
|
+
const decoder = new TextDecoder();
|
|
45
|
+
return decoder.decode(decrypted);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export default AesEncryption;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import V2BrantaClient from "./v2/client.js";
|
|
2
|
+
import { Payment, Destination } from "./v2/client.js";
|
|
3
|
+
import BrantaClientOptions from "./classes/brantaClientOptions.js";
|
|
4
|
+
import BrantaServerBaseUrl from "./classes/brantaServerBaseUrl.js";
|
|
5
|
+
export { V2BrantaClient, BrantaClientOptions, BrantaServerBaseUrl, Payment, Destination };
|
|
6
|
+
export default V2BrantaClient;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import BrantaClientOptions from "../classes/brantaClientOptions.js";
|
|
2
|
+
export interface Destination {
|
|
3
|
+
value: string;
|
|
4
|
+
zk?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface Payment {
|
|
7
|
+
destinations: Destination[];
|
|
8
|
+
ttl?: number;
|
|
9
|
+
description?: string;
|
|
10
|
+
metadata?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface PaymentResponse extends Payment {
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
platform: string;
|
|
15
|
+
platformLogoUrl: string;
|
|
16
|
+
}
|
|
17
|
+
interface PaymentResult {
|
|
18
|
+
payment: PaymentResponse;
|
|
19
|
+
verifyLink: string;
|
|
20
|
+
}
|
|
21
|
+
interface ZKPaymentResult extends PaymentResult {
|
|
22
|
+
secret: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class V2BrantaClient {
|
|
25
|
+
private _defaultOptions;
|
|
26
|
+
constructor(brantaClientOptions: BrantaClientOptions);
|
|
27
|
+
getPayments(address: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
28
|
+
getZKPayment(address: string, secret: string, options?: BrantaClientOptions | null): Promise<Payment[]>;
|
|
29
|
+
addPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<PaymentResult>;
|
|
30
|
+
addZKPayment(payment: Payment, options?: BrantaClientOptions | null): Promise<ZKPaymentResult>;
|
|
31
|
+
isApiKeyValid(options?: BrantaClientOptions | null): Promise<boolean>;
|
|
32
|
+
private _createClient;
|
|
33
|
+
private _setApiKey;
|
|
34
|
+
private _setHmacHeaders;
|
|
35
|
+
}
|
|
36
|
+
export default V2BrantaClient;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import AesEncryption from "../helpers/aes.js";
|
|
2
|
+
import BrantaPaymentException from "../classes/brantaPaymentException.js";
|
|
3
|
+
export class V2BrantaClient {
|
|
4
|
+
constructor(brantaClientOptions) {
|
|
5
|
+
this._defaultOptions = brantaClientOptions;
|
|
6
|
+
}
|
|
7
|
+
async getPayments(address, options = null) {
|
|
8
|
+
const httpClient = this._createClient(options);
|
|
9
|
+
const response = await httpClient.get(`/v2/payments/${address}`);
|
|
10
|
+
if (!response.ok || response.headers.get("content-length") === "0") {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const data = await response.json();
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
async getZKPayment(address, secret, options = null) {
|
|
17
|
+
const payments = await this.getPayments(address, options);
|
|
18
|
+
for (const payment of payments) {
|
|
19
|
+
for (const destination of payment?.destinations || []) {
|
|
20
|
+
if (destination.zk === false)
|
|
21
|
+
continue;
|
|
22
|
+
destination.value = await AesEncryption.decrypt(destination.value, secret);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return payments;
|
|
26
|
+
}
|
|
27
|
+
async addPayment(payment, options = null) {
|
|
28
|
+
const httpClient = this._createClient(options);
|
|
29
|
+
this._setApiKey(httpClient, options);
|
|
30
|
+
await this._setHmacHeaders(httpClient, "POST", "/v2/payments", payment, options);
|
|
31
|
+
const response = await httpClient.post("/v2/payments", payment);
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new BrantaPaymentException(response.status.toString());
|
|
34
|
+
}
|
|
35
|
+
const responseBody = await response.text();
|
|
36
|
+
const paymentResponse = JSON.parse(responseBody);
|
|
37
|
+
const verifyLink = httpClient.baseURL + "/v2/verify/" + encodeURIComponent(payment.destinations[0].value);
|
|
38
|
+
return { payment: paymentResponse, verifyLink };
|
|
39
|
+
}
|
|
40
|
+
async addZKPayment(payment, options = null) {
|
|
41
|
+
const secret = crypto.randomUUID();
|
|
42
|
+
for (const destination of payment?.destinations || []) {
|
|
43
|
+
if (destination.zk === false)
|
|
44
|
+
continue;
|
|
45
|
+
destination.value = await AesEncryption.encrypt(destination.value, secret);
|
|
46
|
+
}
|
|
47
|
+
const responsePayment = (await this.addPayment(payment, options));
|
|
48
|
+
responsePayment.secret = secret;
|
|
49
|
+
responsePayment.verifyLink = responsePayment.verifyLink.replace('verify', 'zk-verify') + "#secret=" + secret;
|
|
50
|
+
return responsePayment;
|
|
51
|
+
}
|
|
52
|
+
async isApiKeyValid(options = null) {
|
|
53
|
+
const httpClient = this._createClient(options);
|
|
54
|
+
this._setApiKey(httpClient, options);
|
|
55
|
+
const response = await httpClient.get("/v2/api-keys/health-check");
|
|
56
|
+
return response.ok;
|
|
57
|
+
}
|
|
58
|
+
_createClient(options) {
|
|
59
|
+
const baseUrl = options?.baseUrl ?? this._defaultOptions?.baseUrl;
|
|
60
|
+
const timeout = options?.timeout ?? this._defaultOptions?.timeout ?? 10000;
|
|
61
|
+
const fullBaseUrl = typeof baseUrl === 'string' ? baseUrl : baseUrl?.url;
|
|
62
|
+
if (!fullBaseUrl) {
|
|
63
|
+
throw new Error("Branta: BaseUrl is a required option.");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
baseURL: fullBaseUrl,
|
|
67
|
+
headers: {},
|
|
68
|
+
timeout,
|
|
69
|
+
async get(url, config = {}) {
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(`${this.baseURL}${url}`, {
|
|
74
|
+
method: "GET",
|
|
75
|
+
headers: { ...this.headers, ...config?.headers },
|
|
76
|
+
signal: config?.signal ?? controller.signal,
|
|
77
|
+
});
|
|
78
|
+
return response;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
82
|
+
throw new BrantaPaymentException('Request timeout');
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async post(url, data, config = {}) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`${this.baseURL}${url}`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
...this.headers,
|
|
99
|
+
...config?.headers,
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(data),
|
|
102
|
+
signal: config?.signal ?? controller.signal,
|
|
103
|
+
});
|
|
104
|
+
return response;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
108
|
+
throw new BrantaPaymentException('Request timeout');
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
_setApiKey(httpClient, options) {
|
|
119
|
+
const apiKey = options?.defaultApiKey ?? this._defaultOptions?.defaultApiKey;
|
|
120
|
+
if (!apiKey) {
|
|
121
|
+
throw new BrantaPaymentException("Unauthorized");
|
|
122
|
+
}
|
|
123
|
+
httpClient.headers = {
|
|
124
|
+
...httpClient.headers,
|
|
125
|
+
Authorization: `Bearer ${apiKey}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async _setHmacHeaders(httpClient, method, url, body, options) {
|
|
129
|
+
const hmacSecret = options?.hmacSecret ?? this._defaultOptions?.hmacSecret;
|
|
130
|
+
if (!hmacSecret) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
134
|
+
const bodyString = JSON.stringify(body);
|
|
135
|
+
const message = `${method}|${httpClient.baseURL}${url}|${bodyString}|${timestamp}`;
|
|
136
|
+
const encoder = new TextEncoder();
|
|
137
|
+
const keyData = encoder.encode(hmacSecret);
|
|
138
|
+
const messageData = encoder.encode(message);
|
|
139
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
140
|
+
const signatureBuffer = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
|
|
141
|
+
const signatureArray = Array.from(new Uint8Array(signatureBuffer));
|
|
142
|
+
const signature = signatureArray
|
|
143
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
144
|
+
.join("")
|
|
145
|
+
.toLowerCase();
|
|
146
|
+
httpClient.headers = {
|
|
147
|
+
...httpClient.headers,
|
|
148
|
+
"X-HMAC-Signature": signature,
|
|
149
|
+
"X-HMAC-Timestamp": timestamp,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export default V2BrantaClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@branta-ops/branta",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "A JavaScript SDK for the Branta API",
|
|
5
5
|
"homepage": "https://github.com/BrantaOps/branta-js#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -13,12 +13,22 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "",
|
|
15
15
|
"type": "module",
|
|
16
|
-
"main": "
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
17
21
|
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
18
23
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --collect-coverage",
|
|
19
|
-
"
|
|
24
|
+
"prepublishOnly": "npm run build",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
20
26
|
},
|
|
21
27
|
"devDependencies": {
|
|
22
|
-
"jest": "^
|
|
28
|
+
"@types/jest": "^29.5.14",
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"jest": "^30.2.0",
|
|
31
|
+
"ts-jest": "^29.4.6",
|
|
32
|
+
"typescript": "^5.0.0"
|
|
23
33
|
}
|
|
24
34
|
}
|
package/jest.config.js
DELETED
package/src/helpers/aes.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
class AesEncryption {
|
|
2
|
-
/**
|
|
3
|
-
* Encrypts a string value using AES-GCM with a secret key
|
|
4
|
-
* @param {string} value - The plaintext to encrypt
|
|
5
|
-
* @param {string} secret - The secret key (will be hashed with SHA-256)
|
|
6
|
-
* @returns {Promise<string>} Base64-encoded encrypted data (iv + ciphertext + tag)
|
|
7
|
-
*/
|
|
8
|
-
static async encrypt(value, secret) {
|
|
9
|
-
const encoder = new TextEncoder();
|
|
10
|
-
const secretData = encoder.encode(secret);
|
|
11
|
-
const keyData = await crypto.subtle.digest('SHA-256', secretData);
|
|
12
|
-
|
|
13
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
14
|
-
|
|
15
|
-
const key = await crypto.subtle.importKey(
|
|
16
|
-
'raw',
|
|
17
|
-
keyData,
|
|
18
|
-
{ name: 'AES-GCM', length: 256 },
|
|
19
|
-
false,
|
|
20
|
-
['encrypt']
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const plaintext = encoder.encode(value);
|
|
24
|
-
const encrypted = await crypto.subtle.encrypt(
|
|
25
|
-
{ name: 'AES-GCM', iv: iv, tagLength: 128 },
|
|
26
|
-
key,
|
|
27
|
-
plaintext
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const encryptedArray = new Uint8Array(encrypted);
|
|
31
|
-
|
|
32
|
-
const ciphertext = encryptedArray.slice(0, -16);
|
|
33
|
-
const tag = encryptedArray.slice(-16);
|
|
34
|
-
|
|
35
|
-
const result = new Uint8Array(iv.length + ciphertext.length + tag.length);
|
|
36
|
-
result.set(iv, 0);
|
|
37
|
-
result.set(ciphertext, iv.length);
|
|
38
|
-
result.set(tag, iv.length + ciphertext.length);
|
|
39
|
-
|
|
40
|
-
return btoa(String.fromCharCode(...result));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Decrypts an encrypted string using AES-GCM with a secret key
|
|
45
|
-
* @param {string} encryptedValue - Base64-encoded encrypted data
|
|
46
|
-
* @param {string} secret - The secret key (will be hashed with SHA-256)
|
|
47
|
-
* @returns {Promise<string>} The decrypted plaintext
|
|
48
|
-
*/
|
|
49
|
-
static async decrypt(encryptedValue, secret) {
|
|
50
|
-
const encryptedData = Uint8Array.from(atob(encryptedValue), c => c.charCodeAt(0));
|
|
51
|
-
|
|
52
|
-
const encoder = new TextEncoder();
|
|
53
|
-
const secretData = encoder.encode(secret);
|
|
54
|
-
const keyData = await crypto.subtle.digest('SHA-256', secretData);
|
|
55
|
-
|
|
56
|
-
const iv = encryptedData.slice(0, 12);
|
|
57
|
-
const tag = encryptedData.slice(-16);
|
|
58
|
-
const ciphertext = encryptedData.slice(12, -16);
|
|
59
|
-
|
|
60
|
-
const ciphertextWithTag = new Uint8Array(ciphertext.length + tag.length);
|
|
61
|
-
ciphertextWithTag.set(ciphertext, 0);
|
|
62
|
-
ciphertextWithTag.set(tag, ciphertext.length);
|
|
63
|
-
|
|
64
|
-
const key = await crypto.subtle.importKey(
|
|
65
|
-
'raw',
|
|
66
|
-
keyData,
|
|
67
|
-
{ name: 'AES-GCM', length: 256 },
|
|
68
|
-
false,
|
|
69
|
-
['decrypt']
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const decrypted = await crypto.subtle.decrypt(
|
|
73
|
-
{ name: 'AES-GCM', iv: iv, tagLength: 128 },
|
|
74
|
-
key,
|
|
75
|
-
ciphertextWithTag
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const decoder = new TextDecoder();
|
|
79
|
-
return decoder.decode(decrypted);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export default AesEncryption;
|
package/src/index.js
DELETED
package/src/v2/client.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import AesEncryption from "../helpers/aes.js";
|
|
2
|
-
import BrantaPaymentException from "../classes/brantaPaymentException.js";
|
|
3
|
-
|
|
4
|
-
export class V2BrantaClient {
|
|
5
|
-
constructor(brantaClientOptions) {
|
|
6
|
-
this._defaultOptions = brantaClientOptions;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
async getPayments(address, options = null) {
|
|
10
|
-
const httpClient = this._createClient(options);
|
|
11
|
-
const response = await httpClient.get(`/v2/payments/${address}`);
|
|
12
|
-
|
|
13
|
-
if (!response.ok || response.headers.get("content-length") === "0") {
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const data = await response.json();
|
|
18
|
-
return data;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async getZKPayment(address, secret, options = null) {
|
|
22
|
-
const payments = await this.getPayments(address, options);
|
|
23
|
-
|
|
24
|
-
for (const payment of payments) {
|
|
25
|
-
for (const destination of payment.destinations) {
|
|
26
|
-
if (destination.isZk === false) continue;
|
|
27
|
-
destination.value = await AesEncryption.decrypt(destination.value, secret);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return payments;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async addPayment(payment, options = null) {
|
|
35
|
-
const httpClient = this._createClient(options);
|
|
36
|
-
this._setApiKey(httpClient, options);
|
|
37
|
-
|
|
38
|
-
const response = await httpClient.post("/v2/payments", payment);
|
|
39
|
-
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
throw new BrantaPaymentException(response.status.toString());
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const responseBody = await response.text();
|
|
45
|
-
return JSON.parse(responseBody);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async addZKPayment(payment, options = null) {
|
|
49
|
-
const secret = crypto.randomUUID();
|
|
50
|
-
|
|
51
|
-
for (const destination of payment.destinations) {
|
|
52
|
-
if (destination.isZk === false) continue;
|
|
53
|
-
destination.value = await AesEncryption.encrypt(destination.value, secret);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const responsePayment = await this.addPayment(payment, options);
|
|
57
|
-
return { payment: responsePayment, secret };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async isApiKeyValid(options = null) {
|
|
61
|
-
const httpClient = this._createClient(options);
|
|
62
|
-
this._setApiKey(httpClient, options);
|
|
63
|
-
|
|
64
|
-
const response = await fetch(
|
|
65
|
-
`${httpClient.baseURL}/v2/api-keys/health-check`,
|
|
66
|
-
{
|
|
67
|
-
headers: httpClient.headers,
|
|
68
|
-
},
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
return response.ok;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
_createClient(options) {
|
|
75
|
-
const baseUrl = options?.baseUrl ?? this._defaultOptions?.baseUrl;
|
|
76
|
-
|
|
77
|
-
if (!baseUrl?.url) {
|
|
78
|
-
throw new Error("Branta: BaseUrl is a required option.");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const fullBaseUrl = baseUrl.url;
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
baseURL: fullBaseUrl,
|
|
85
|
-
headers: {},
|
|
86
|
-
async get(url, config = {}) {
|
|
87
|
-
const response = await fetch(`${this.baseURL}${url}`, {
|
|
88
|
-
method: "GET",
|
|
89
|
-
headers: { ...this.headers, ...config?.headers },
|
|
90
|
-
signal: config?.signal,
|
|
91
|
-
});
|
|
92
|
-
return response;
|
|
93
|
-
},
|
|
94
|
-
async post(url, data, config = {}) {
|
|
95
|
-
const response = await fetch(`${this.baseURL}${url}`, {
|
|
96
|
-
method: "POST",
|
|
97
|
-
headers: {
|
|
98
|
-
"Content-Type": "application/json",
|
|
99
|
-
...this.headers,
|
|
100
|
-
...config?.headers,
|
|
101
|
-
},
|
|
102
|
-
body: JSON.stringify(data),
|
|
103
|
-
signal: config?.signal,
|
|
104
|
-
});
|
|
105
|
-
return response;
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
_setApiKey(httpClient, options) {
|
|
111
|
-
const apiKey =
|
|
112
|
-
options?.defaultApiKey ?? this._defaultOptions?.defaultApiKey;
|
|
113
|
-
|
|
114
|
-
if (!apiKey) {
|
|
115
|
-
throw new BrantaPaymentException("Unauthorized");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
httpClient.headers = {
|
|
119
|
-
...httpClient.headers,
|
|
120
|
-
Authorization: `Bearer ${apiKey}`,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export default V2BrantaClient;
|
package/test/helpers/aes.test.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "@jest/globals";
|
|
2
|
-
import AesEncryption from "./../../src/helpers/aes";
|
|
3
|
-
|
|
4
|
-
describe("AesEncryption", () => {
|
|
5
|
-
test("should encrypt and decrypt a bitcoin address", async () => {
|
|
6
|
-
const plaintext = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
|
|
7
|
-
const secret = "mySecret123";
|
|
8
|
-
|
|
9
|
-
const encrypted = await AesEncryption.encrypt(plaintext, secret);
|
|
10
|
-
const decrypted = await AesEncryption.decrypt(encrypted, secret);
|
|
11
|
-
|
|
12
|
-
expect(decrypted).toBe(plaintext);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("should produce different ciphertext with different secrets", async () => {
|
|
16
|
-
const plaintext = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
|
|
17
|
-
const secret1 = "secret1";
|
|
18
|
-
const secret2 = "secret2";
|
|
19
|
-
|
|
20
|
-
const encrypted1 = await AesEncryption.encrypt(plaintext, secret1);
|
|
21
|
-
const encrypted2 = await AesEncryption.encrypt(plaintext, secret2);
|
|
22
|
-
|
|
23
|
-
expect(encrypted1).not.toBe(encrypted2);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("should decrypt text", async () => {
|
|
27
|
-
const encrypted =
|
|
28
|
-
"pQerSFV+fievHP+guYoGJjx1CzFFrYWHAgWrLhn5473Z19M6+WMScLd1hsk808AEF/x+GpZKmNacFBf5BbQ=";
|
|
29
|
-
const secret1 = "1234";
|
|
30
|
-
|
|
31
|
-
const decrypted = await AesEncryption.decrypt(encrypted, secret1);
|
|
32
|
-
|
|
33
|
-
expect(decrypted).toBe("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
|
|
34
|
-
});
|
|
35
|
-
});
|
package/test/script.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import * as branta from "branta";
|
|
2
|
-
|
|
3
|
-
const client = new branta.V2BrantaClient(
|
|
4
|
-
new branta.BrantaClientOptions({
|
|
5
|
-
baseUrl: branta.BrantaBaseServerUrl.Localhost,
|
|
6
|
-
defaultApiKey:
|
|
7
|
-
"<api-key-here>",
|
|
8
|
-
}),
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
var payments = await client.getPayments(
|
|
12
|
-
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
|
|
13
|
-
);
|
|
14
|
-
console.log(payments);
|
|
15
|
-
|
|
16
|
-
if (payments.length == 0) {
|
|
17
|
-
console.log("Creating Payment...");
|
|
18
|
-
await client.addPayment({
|
|
19
|
-
description: "Testing description",
|
|
20
|
-
destinations: [
|
|
21
|
-
{
|
|
22
|
-
value: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
|
|
23
|
-
zk: false,
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
ttl: "600",
|
|
27
|
-
});
|
|
28
|
-
}
|
package/test/v2/client.test.js
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, jest, beforeEach } from "@jest/globals";
|
|
2
|
-
import V2BrantaClient from "../../src/v2/client.js";
|
|
3
|
-
import BrantaPaymentException from "../../src/classes/brantaPaymentException.js";
|
|
4
|
-
import AesEncryption from "../../src/helpers/aes.js";
|
|
5
|
-
|
|
6
|
-
describe("V2BrantaClient", () => {
|
|
7
|
-
let client;
|
|
8
|
-
let mockFetch;
|
|
9
|
-
const defaultOptions = {
|
|
10
|
-
baseUrl: { url: "http://localhost:3000" },
|
|
11
|
-
defaultApiKey: "test-api-key",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const testPayments = [
|
|
15
|
-
{
|
|
16
|
-
destinations: [
|
|
17
|
-
{
|
|
18
|
-
value: "123",
|
|
19
|
-
isZk: false,
|
|
20
|
-
},
|
|
21
|
-
],
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
destinations: [
|
|
25
|
-
{
|
|
26
|
-
value: "456",
|
|
27
|
-
isZk: false,
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
client = new V2BrantaClient(defaultOptions);
|
|
35
|
-
mockFetch = jest.fn();
|
|
36
|
-
global.fetch = mockFetch;
|
|
37
|
-
jest.clearAllMocks();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("getPayments", () => {
|
|
41
|
-
test("should return payments", async () => {
|
|
42
|
-
const address = "test-address";
|
|
43
|
-
mockFetch.mockResolvedValue({
|
|
44
|
-
ok: true,
|
|
45
|
-
headers: {
|
|
46
|
-
get: () => "100",
|
|
47
|
-
},
|
|
48
|
-
json: async () => testPayments,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const result = await client.getPayments(address);
|
|
52
|
-
|
|
53
|
-
expect(result).not.toBeNull();
|
|
54
|
-
expect(result).toHaveLength(2);
|
|
55
|
-
expect(result[0].destinations[0].value).toBe("123");
|
|
56
|
-
expect(result[1].destinations[0].value).toBe("456");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("should return empty list on non-success status code", async () => {
|
|
60
|
-
const address = "test-address";
|
|
61
|
-
mockFetch.mockResolvedValue({
|
|
62
|
-
ok: false,
|
|
63
|
-
headers: {
|
|
64
|
-
get: () => "0",
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const result = await client.getPayments(address);
|
|
69
|
-
|
|
70
|
-
expect(result).not.toBeNull();
|
|
71
|
-
expect(result).toHaveLength(0);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("should return empty list on null content", async () => {
|
|
75
|
-
const address = "test-address";
|
|
76
|
-
mockFetch.mockResolvedValue({
|
|
77
|
-
ok: true,
|
|
78
|
-
headers: {
|
|
79
|
-
get: () => "0",
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const result = await client.getPayments(address);
|
|
84
|
-
|
|
85
|
-
expect(result).not.toBeNull();
|
|
86
|
-
expect(result).toHaveLength(0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("should use custom options", async () => {
|
|
90
|
-
const address = "test-address";
|
|
91
|
-
const customOptions = {
|
|
92
|
-
baseUrl: { url: "https://production.example.com" },
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
mockFetch.mockResolvedValue({
|
|
96
|
-
ok: true,
|
|
97
|
-
headers: {
|
|
98
|
-
get: () => "2",
|
|
99
|
-
},
|
|
100
|
-
json: async () => [],
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
await client.getPayments(address, customOptions);
|
|
104
|
-
|
|
105
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
106
|
-
"https://production.example.com/v2/payments/test-address",
|
|
107
|
-
expect.any(Object),
|
|
108
|
-
);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("should throw exception if baseUrl not set", async () => {
|
|
112
|
-
client = new V2BrantaClient({});
|
|
113
|
-
|
|
114
|
-
await expect(client.getPayments("test-address")).rejects.toThrow(
|
|
115
|
-
"Branta: BaseUrl is a required option.",
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("getZKPayment", () => {
|
|
121
|
-
test("should decrypt ZK destination values", async () => {
|
|
122
|
-
const encryptedValue =
|
|
123
|
-
"pQerSFV+fievHP+guYoGJjx1CzFFrYWHAgWrLhn5473Z19M6+WMScLd1hsk808AEF/x+GpZKmNacFBf5BbQ=";
|
|
124
|
-
const payments = [
|
|
125
|
-
{
|
|
126
|
-
destinations: [
|
|
127
|
-
{ isZk: true, value: encryptedValue },
|
|
128
|
-
{ isZk: false, value: "plain-value" },
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
mockFetch.mockResolvedValue({
|
|
134
|
-
ok: true,
|
|
135
|
-
headers: {
|
|
136
|
-
get: () => "100",
|
|
137
|
-
},
|
|
138
|
-
json: async () => JSON.parse(JSON.stringify(payments)),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const result = await client.getZKPayment(encryptedValue, "1234");
|
|
142
|
-
|
|
143
|
-
expect(result).not.toBeNull();
|
|
144
|
-
expect(result).toHaveLength(1);
|
|
145
|
-
expect(result[0].destinations[0].value).toBe(
|
|
146
|
-
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("should return unmodified payments with no ZK destinations", async () => {
|
|
151
|
-
const payments = [
|
|
152
|
-
{
|
|
153
|
-
destinations: [{ isZk: false, value: "plain-value" }],
|
|
154
|
-
},
|
|
155
|
-
];
|
|
156
|
-
|
|
157
|
-
mockFetch.mockResolvedValue({
|
|
158
|
-
ok: true,
|
|
159
|
-
headers: {
|
|
160
|
-
get: () => "100",
|
|
161
|
-
},
|
|
162
|
-
json: async () => JSON.parse(JSON.stringify(payments)),
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const result = await client.getZKPayment("plain-value", "test-secret");
|
|
166
|
-
|
|
167
|
-
expect(result).not.toBeNull();
|
|
168
|
-
expect(result).toHaveLength(1);
|
|
169
|
-
expect(result[0].destinations[0].value).toBe("plain-value");
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe("addPayment", () => {
|
|
174
|
-
test("should throw exception when no API key is provided", async () => {
|
|
175
|
-
const payment = testPayments[0];
|
|
176
|
-
const clientWithoutApiKey = new V2BrantaClient({
|
|
177
|
-
baseUrl: { url: "https://production.example.com" },
|
|
178
|
-
defaultApiKey: null,
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
await expect(clientWithoutApiKey.addPayment(payment)).rejects.toThrow(
|
|
182
|
-
BrantaPaymentException,
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("should use custom API key", async () => {
|
|
187
|
-
const payment = testPayments[0];
|
|
188
|
-
const customOptions = {
|
|
189
|
-
baseUrl: { url: "https://production.example.com" },
|
|
190
|
-
defaultApiKey: "custom-api-key",
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
mockFetch.mockResolvedValue({
|
|
194
|
-
ok: true,
|
|
195
|
-
text: async () => JSON.stringify(testPayments[0]),
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
await client.addPayment(payment, customOptions);
|
|
199
|
-
|
|
200
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
201
|
-
"https://production.example.com/v2/payments",
|
|
202
|
-
expect.objectContaining({
|
|
203
|
-
headers: expect.objectContaining({
|
|
204
|
-
Authorization: "Bearer custom-api-key",
|
|
205
|
-
}),
|
|
206
|
-
}),
|
|
207
|
-
);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("should throw exception on failed response", async () => {
|
|
211
|
-
const payment = testPayments[0];
|
|
212
|
-
|
|
213
|
-
mockFetch.mockResolvedValue({
|
|
214
|
-
ok: false,
|
|
215
|
-
status: 400,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
await expect(client.addPayment(payment)).rejects.toThrow(
|
|
219
|
-
BrantaPaymentException,
|
|
220
|
-
);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("should return parsed response on success", async () => {
|
|
224
|
-
const payment = testPayments[0];
|
|
225
|
-
const expectedResponse = { ...payment, id: "12345" };
|
|
226
|
-
|
|
227
|
-
mockFetch.mockResolvedValue({
|
|
228
|
-
ok: true,
|
|
229
|
-
text: async () => JSON.stringify(expectedResponse),
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const result = await client.addPayment(payment);
|
|
233
|
-
|
|
234
|
-
expect(result).toEqual(expectedResponse);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe("addZKPayment", () => {
|
|
239
|
-
test("should encrypt ZK destinations and return payment with secret", async () => {
|
|
240
|
-
const plainText = "plain-value";
|
|
241
|
-
const payment = {
|
|
242
|
-
destinations: [
|
|
243
|
-
{ isZk: true, value: plainText },
|
|
244
|
-
{ isZk: false, value: "other-value" },
|
|
245
|
-
],
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
mockFetch.mockResolvedValue({
|
|
249
|
-
ok: true,
|
|
250
|
-
text: async () => JSON.stringify(payment),
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const result = await client.addZKPayment(payment);
|
|
254
|
-
|
|
255
|
-
const zkPayment = result.payment.destinations.find(
|
|
256
|
-
(d) => d.isZk == true,
|
|
257
|
-
).value;
|
|
258
|
-
|
|
259
|
-
expect(await AesEncryption.decrypt(zkPayment, result.secret)).toBe(
|
|
260
|
-
plainText,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
expect(result.payment).toBeDefined();
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe("isApiKeyValid", () => {
|
|
268
|
-
test("should return true for valid API key", async () => {
|
|
269
|
-
mockFetch.mockResolvedValue({
|
|
270
|
-
ok: true,
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const result = await client.isApiKeyValid();
|
|
274
|
-
|
|
275
|
-
expect(result).toBe(true);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("should return false for invalid API key", async () => {
|
|
279
|
-
mockFetch.mockResolvedValue({
|
|
280
|
-
ok: false,
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const result = await client.isApiKeyValid();
|
|
284
|
-
|
|
285
|
-
expect(result).toBe(false);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("should use custom options", async () => {
|
|
289
|
-
const customOptions = {
|
|
290
|
-
baseUrl: { url: "https://production.example.com" },
|
|
291
|
-
defaultApiKey: "custom-key",
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
mockFetch.mockResolvedValue({
|
|
295
|
-
ok: true,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
await client.isApiKeyValid(customOptions);
|
|
299
|
-
|
|
300
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
301
|
-
"https://production.example.com/v2/api-keys/health-check",
|
|
302
|
-
expect.objectContaining({
|
|
303
|
-
headers: expect.objectContaining({
|
|
304
|
-
Authorization: "Bearer custom-key",
|
|
305
|
-
}),
|
|
306
|
-
}),
|
|
307
|
-
);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
});
|