@branta-ops/branta 0.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 +54 -0
- package/jest.config.js +4 -0
- package/package.json +24 -0
- package/src/classes/brantaBaseServerUrl.js +7 -0
- package/src/classes/brantaClientOptions.js +10 -0
- package/src/classes/brantaPaymentException.js +8 -0
- package/src/helpers/aes.js +83 -0
- package/src/index.js +5 -0
- package/src/v2/client.js +125 -0
- package/test/helpers/aes.test.js +35 -0
- package/test/script.js +28 -0
- package/test/v2/client.test.js +310 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Branta JavaScript SDK
|
|
2
|
+
|
|
3
|
+
Package contains functionality to assist JavaScript projects with making requests to Branta's server.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install via npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install branta
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
```js
|
|
15
|
+
import * as branta from "branta";
|
|
16
|
+
|
|
17
|
+
const client = new branta.V2BrantaClient(
|
|
18
|
+
new branta.BrantaClientOptions({
|
|
19
|
+
baseUrl: branta.BrantaBaseServerUrl.Localhost,
|
|
20
|
+
defaultApiKey:
|
|
21
|
+
"<api-key-here>",
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
var payments = await client.getPayments(
|
|
26
|
+
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
|
|
27
|
+
);
|
|
28
|
+
console.log(payments);
|
|
29
|
+
|
|
30
|
+
if (payments.length == 0) {
|
|
31
|
+
console.log("Creating Payment...");
|
|
32
|
+
await client.addPayment({
|
|
33
|
+
description: "Testing description",
|
|
34
|
+
destinations: [
|
|
35
|
+
{
|
|
36
|
+
value: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
|
|
37
|
+
zk: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
ttl: "600",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Feature Support
|
|
46
|
+
|
|
47
|
+
- [X] Per Environment configuration
|
|
48
|
+
- [X] V2 Get Payment by address
|
|
49
|
+
- [ ] V2 Get Payment by QR Code
|
|
50
|
+
- [X] V2 Get decrypted Zero Knowledge by address and secret
|
|
51
|
+
- [X] V2 Add Payment
|
|
52
|
+
- [ ] V2 Payment by Parent Platform with HMAC
|
|
53
|
+
- [X] V2 Add Zero Knowledge Payment with secret
|
|
54
|
+
- [X] V2 Check API key valid
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@branta-ops/branta",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A JavaScript SDK for the Branta API",
|
|
5
|
+
"homepage": "https://github.com/BrantaOps/branta-js#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/BrantaOps/branta-js/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/BrantaOps/branta-js.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "src/index.js",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --collect-coverage",
|
|
19
|
+
"script": "npm link branta && node test/script.js"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"jest": "^30.2.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
ADDED
package/src/v2/client.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
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;
|
|
@@ -0,0 +1,35 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
});
|