From 5e7b1d56afe5f61c9cfd4e3dbd4b6c42048e4771 Mon Sep 17 00:00:00 2001 From: Polybius93 <99192647+Polybius93@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:52:44 +0200 Subject: [PATCH] feat: remove unnecessary attestor handler (#20) * feat: replace attestor handler with attestor functions * feat: modify attestor request functions, add tests * revert: 1.0.23 package update * 2.0.0 --- package.json | 5 +- src/attestor-handlers/attestor-handler.ts | 75 ---------- .../attestor/attestor-request.functions.ts | 65 ++++++++ src/functions/attestor/index.ts | 4 + src/functions/request/request.functions.ts | 11 ++ src/index.ts | 2 - src/models/attestor.models.ts | 2 +- tests/unit/attestor-request-function.test.ts | 139 ++++++++++++++++++ tests/unit/request-functions.test.ts | 43 ++++++ yarn.lock | 24 +++ 10 files changed, 291 insertions(+), 79 deletions(-) delete mode 100644 src/attestor-handlers/attestor-handler.ts create mode 100644 src/functions/attestor/attestor-request.functions.ts create mode 100644 src/functions/attestor/index.ts create mode 100644 src/functions/request/request.functions.ts create mode 100644 tests/unit/attestor-request-function.test.ts create mode 100644 tests/unit/request-functions.test.ts diff --git a/package.json b/package.json index 5d6cb2a..bd90b8c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "1.0.22", + "version": "2.0.0", "description": "This library provides a comprehensive set of interfaces and functions for minting dlcBTC tokens on supported blockchains.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -14,6 +14,7 @@ "./constants": "./dist/constants/index.js", "./models": "./dist/models/index.js", "./bitcoin-functions": "./dist/functions/bitcoin/index.js", + "./attestor-request-functions": "./dist/functions/attestor/index.js", "./ethereum-functions": "./dist/functions/ethereum/index.js" }, "scripts": { @@ -61,6 +62,7 @@ "@noble/hashes": "^1.4.0", "@scure/base": "^1.1.6", "@scure/btc-signer": "^1.3.1", + "@types/ramda": "^0.30.1", "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.5", "chalk": "^5.3.0", @@ -68,6 +70,7 @@ "ethers": "5.7.2", "ledger-bitcoin": "^0.2.3", "prompts": "^2.4.2", + "ramda": "^0.30.1", "scure": "^1.6.0", "tiny-secp256k1": "^2.2.3" } diff --git a/src/attestor-handlers/attestor-handler.ts b/src/attestor-handlers/attestor-handler.ts deleted file mode 100644 index e29bd32..0000000 --- a/src/attestor-handlers/attestor-handler.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { FundingTXAttestorInfo, WithdrawDepositTXAttestorInfo } from '../models/attestor.models.js'; -import { AttestorError } from '../models/errors.js'; - -export class AttestorHandler { - private attestorRootURLs: string[]; - - constructor(attestorRootURLs: string[]) { - this.attestorRootURLs = attestorRootURLs; - } - - private async sendRequest(url: string, body: string): Promise { - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, - body, - }); - if (!response.ok) { - throw new AttestorError(`Attestor Response ${url} was not OK: ${response.statusText}`); - } - return true; - } - - async submitFundingPSBT(fundingTXAttestorInfo: FundingTXAttestorInfo): Promise { - const fundingEndpoints = this.attestorRootURLs.map(url => `${url}/app/create-psbt-event`); - - const body = JSON.stringify({ - uuid: fundingTXAttestorInfo.vaultUUID, - funding_transaction_psbt: fundingTXAttestorInfo.fundingPSBT, - mint_address: fundingTXAttestorInfo.userEthereumAddress, - chain: fundingTXAttestorInfo.attestorChainID, - alice_pubkey: fundingTXAttestorInfo.userBitcoinTaprootPublicKey, - }); - - const attestorResponses: (boolean | string)[] = await Promise.all( - fundingEndpoints.map(async url => - this.sendRequest(url, body) - .then(response => response) - .catch(error => error.message) - ) - ); - - if (attestorResponses.every(response => response !== true)) { - throw new AttestorError( - `Error sending [Funding] Transaction to Attestors: - ${attestorResponses.join('| ')}` - ); - } - } - - async submitWithdrawDepositPSBT( - withdrawDepositTXAttestorInfo: WithdrawDepositTXAttestorInfo - ): Promise { - const depositWithdrawEndpoints = this.attestorRootURLs.map(url => `${url}/app/withdraw`); - - const body = JSON.stringify({ - uuid: withdrawDepositTXAttestorInfo.vaultUUID, - wd_psbt: withdrawDepositTXAttestorInfo.depositWithdrawPSBT, - }); - - const attestorResponses: (boolean | string)[] = await Promise.all( - depositWithdrawEndpoints.map(async url => - this.sendRequest(url, body) - .then(response => response) - .catch(error => error.message) - ) - ); - - if (attestorResponses.every(response => response !== true)) { - throw new AttestorError( - `Error sending [Deposit/Withdraw] Transaction to Attestors: - ${attestorResponses.join('| ')}` - ); - } - } -} diff --git a/src/functions/attestor/attestor-request.functions.ts b/src/functions/attestor/attestor-request.functions.ts new file mode 100644 index 0000000..64992bc --- /dev/null +++ b/src/functions/attestor/attestor-request.functions.ts @@ -0,0 +1,65 @@ +import { equals, filter, isEmpty, isNotNil, join, map, prop } from 'ramda'; + +import { + FundingTXAttestorInfo, + WithdrawDepositTXAttestorInfo, +} from '../../models/attestor.models.js'; +import { AttestorError } from '../../models/errors.js'; +import { sendRequest } from '../request/request.functions.js'; + +export async function submitFundingPSBT( + attestorRootURLs: string[], + fundingTXAttestorInfo: FundingTXAttestorInfo +): Promise { + await submitPSBT(attestorRootURLs, fundingTXAttestorInfo, '/app/create-psbt-event', info => ({ + uuid: info.vaultUUID, + funding_transaction_psbt: info.fundingPSBT, + mint_address: info.userEthereumAddress, + chain: info.attestorChainID, + alice_pubkey: info.userBitcoinTaprootPublicKey, + })); +} + +export async function submitWithdrawDepositPSBT( + attestorRootURLs: string[], + withdrawDepositTXAttestorInfo: WithdrawDepositTXAttestorInfo +): Promise { + await submitPSBT(attestorRootURLs, withdrawDepositTXAttestorInfo, '/app/withdraw', info => ({ + uuid: info.vaultUUID, + wd_psbt: info.withdrawDepositPSBT, + })); +} + +export async function submitPSBT( + attestorRootURLs: string[], + transactionInfo: T, + endpointPath: string, + transformBody: (transactionInfo: T) => object +): Promise { + if (isEmpty(attestorRootURLs)) { + throw new AttestorError('No Attestor URLs provided'); + } + + const endpoints: string[] = attestorRootURLs.map(url => `${url}${endpointPath}`); + const requestBody: string = JSON.stringify(transformBody(transactionInfo)); + + await sendAndProcessRequests(endpoints, requestBody); +} + +const sendAndProcessRequests = async (attestorEndpoints: string[], requestBody: string) => { + const attestorErrorResponses: string[] = filter( + isNotNil, + await Promise.all( + map( + url => sendRequest(url, requestBody).catch(error => prop('message', error)), + attestorEndpoints + ) + ) + ); + + if (equals(attestorEndpoints.length, attestorErrorResponses.length)) { + throw new AttestorError( + `Error sending Transaction to Attestors: ${join('|', attestorErrorResponses)}` + ); + } +}; diff --git a/src/functions/attestor/index.ts b/src/functions/attestor/index.ts new file mode 100644 index 0000000..8b032cd --- /dev/null +++ b/src/functions/attestor/index.ts @@ -0,0 +1,4 @@ +export { + submitFundingPSBT, + submitWithdrawDepositPSBT, +} from '../attestor/attestor-request.functions.js'; diff --git a/src/functions/request/request.functions.ts b/src/functions/request/request.functions.ts new file mode 100644 index 0000000..5955b3b --- /dev/null +++ b/src/functions/request/request.functions.ts @@ -0,0 +1,11 @@ +export async function sendRequest(url: string, body: string): Promise { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, + body, + }); + + if (!response.ok) { + throw new Error(`Response ${url} was not OK: ${response.statusText}`); + } +} diff --git a/src/index.ts b/src/index.ts index 0f82efd..24adf02 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -import { AttestorHandler } from './attestor-handlers/attestor-handler.js'; import { LedgerDLCHandler } from './dlc-handlers/ledger-dlc-handler.js'; import { PrivateKeyDLCHandler } from './dlc-handlers/private-key-dlc-handler.js'; import { SoftwareWalletDLCHandler } from './dlc-handlers/software-wallet-dlc-handler.js'; @@ -12,6 +11,5 @@ export { SoftwareWalletDLCHandler, EthereumHandler, ReadOnlyEthereumHandler, - AttestorHandler, ProofOfReserveHandler, }; diff --git a/src/models/attestor.models.ts b/src/models/attestor.models.ts index 8ba4653..538fd1e 100644 --- a/src/models/attestor.models.ts +++ b/src/models/attestor.models.ts @@ -15,5 +15,5 @@ export interface FundingTXAttestorInfo { export interface WithdrawDepositTXAttestorInfo { vaultUUID: string; - depositWithdrawPSBT: string; + withdrawDepositPSBT: string; } diff --git a/tests/unit/attestor-request-function.test.ts b/tests/unit/attestor-request-function.test.ts new file mode 100644 index 0000000..5f93c9d --- /dev/null +++ b/tests/unit/attestor-request-function.test.ts @@ -0,0 +1,139 @@ +import { + submitFundingPSBT, + submitWithdrawDepositPSBT, +} from '../../src/functions/attestor/attestor-request.functions'; +import * as requestFunctions from '../../src/functions/request/request.functions'; +import { + FundingTXAttestorInfo, + WithdrawDepositTXAttestorInfo, +} from '../../src/models/attestor.models'; +import { AttestorError } from '../../src/models/errors'; +import { TEST_REGTEST_ATTESTOR_APIS } from '../mocks/api.test.constants'; + +describe('Attestor Request Sending', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('submitFundingPSBT', () => { + const fundingTXAttestorInfo: FundingTXAttestorInfo = { + vaultUUID: 'vault-uuid', + fundingPSBT: 'funding-psbt', + userEthereumAddress: 'user-ethereum-address', + userBitcoinTaprootPublicKey: 'user-bitcoin-taproot-public-key', + attestorChainID: 'evm-arbitrum', + }; + it('should succeed without errors when all requests are successful', async () => { + jest + .spyOn(requestFunctions, 'sendRequest') + .mockImplementationOnce(async () => {}) + .mockImplementationOnce(async () => {}) + .mockImplementationOnce(async () => {}); + + await expect( + submitFundingPSBT(TEST_REGTEST_ATTESTOR_APIS, fundingTXAttestorInfo) + ).resolves.not.toThrow(); + }); + + it('should not throw an error if not all requests are successful', async () => { + jest + .spyOn(requestFunctions, 'sendRequest') + .mockImplementationOnce(async () => { + throw new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[0]} was not OK`); + }) + .mockImplementationOnce(async () => {}) + .mockImplementationOnce(async () => {}); + + await expect( + submitFundingPSBT(TEST_REGTEST_ATTESTOR_APIS, fundingTXAttestorInfo) + ).resolves.not.toThrow(); + }); + + it('should throw an error if all requests fail', async () => { + jest + .spyOn(requestFunctions, 'sendRequest') + .mockImplementationOnce(async () => { + throw new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[0]} was not OK`); + }) + .mockImplementationOnce(async () => { + throw new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[1]} was not OK`); + }) + .mockImplementationOnce(async () => { + throw new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[2]} was not OK`); + }); + + await expect( + submitFundingPSBT(TEST_REGTEST_ATTESTOR_APIS, fundingTXAttestorInfo) + ).rejects.toThrow( + new AttestorError( + `Error sending Transaction to Attestors: Response ${TEST_REGTEST_ATTESTOR_APIS[0]} was not OK|Response ${TEST_REGTEST_ATTESTOR_APIS[1]} was not OK|Response ${TEST_REGTEST_ATTESTOR_APIS[2]} was not OK` + ) + ); + }); + + it('should raise an error when the attestorURLs parameter is empty', async () => { + await expect(submitFundingPSBT([], fundingTXAttestorInfo)).rejects.toThrow( + new AttestorError('No Attestor URLs provided') + ); + }); + }); + describe('submitWithdrawDepositPSBT', () => { + const withdrawDepositTXAttestorInfo: WithdrawDepositTXAttestorInfo = { + vaultUUID: 'vault-uuid', + withdrawDepositPSBT: 'deposit-withdraw-psbt', + }; + + it('should succeed without errors when all requests are successful', async () => { + jest + .spyOn(requestFunctions, 'sendRequest') + .mockImplementationOnce(async () => {}) + .mockImplementationOnce(async () => {}) + .mockImplementationOnce(async () => {}); + + await expect( + submitWithdrawDepositPSBT(TEST_REGTEST_ATTESTOR_APIS, withdrawDepositTXAttestorInfo) + ).resolves.not.toThrow(); + }); + + it('should not throw an error if not all requests are successful', async () => { + jest + .spyOn(requestFunctions, 'sendRequest') + .mockImplementationOnce(async () => { + throw new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[0]} was not OK`); + }) + .mockImplementationOnce(async () => {}) + .mockImplementationOnce(async () => {}); + + await expect( + submitWithdrawDepositPSBT(TEST_REGTEST_ATTESTOR_APIS, withdrawDepositTXAttestorInfo) + ).resolves.not.toThrow(); + }); + + it('should throw an error if all requests fail', async () => { + jest + .spyOn(requestFunctions, 'sendRequest') + .mockImplementationOnce(async () => { + throw new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[0]} was not OK`); + }) + .mockImplementationOnce(async () => { + throw new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[1]} was not OK`); + }) + .mockImplementationOnce(async () => { + throw new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[2]} was not OK`); + }); + + await expect( + submitWithdrawDepositPSBT(TEST_REGTEST_ATTESTOR_APIS, withdrawDepositTXAttestorInfo) + ).rejects.toThrow( + new AttestorError( + `Error sending Transaction to Attestors: Response ${TEST_REGTEST_ATTESTOR_APIS[0]} was not OK|Response ${TEST_REGTEST_ATTESTOR_APIS[1]} was not OK|Response ${TEST_REGTEST_ATTESTOR_APIS[2]} was not OK` + ) + ); + }); + + it('should raise an error when the attestorURLs parameter is empty', async () => { + await expect(submitWithdrawDepositPSBT([], withdrawDepositTXAttestorInfo)).rejects.toThrow( + new AttestorError('No Attestor URLs provided') + ); + }); + }); +}); diff --git a/tests/unit/request-functions.test.ts b/tests/unit/request-functions.test.ts new file mode 100644 index 0000000..b7c0098 --- /dev/null +++ b/tests/unit/request-functions.test.ts @@ -0,0 +1,43 @@ +import { sendRequest } from '../../src/functions/request/request.functions'; +import { TEST_REGTEST_ATTESTOR_APIS } from '../mocks/api.test.constants'; + +global.fetch = jest.fn(); + +describe('Request Functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('sendRequest', () => { + it('should not result in an error when the response status is ok', async () => { + jest + .spyOn(global, 'fetch') + .mockImplementationOnce(async () => new Response(null, { status: 200 })); + + await expect( + sendRequest(TEST_REGTEST_ATTESTOR_APIS[0], 'requestBody') + ).resolves.not.toThrow(); + }); + + it('should result in an error when the response status is not ok', async () => { + jest + .spyOn(global, 'fetch') + .mockImplementationOnce( + async () => new Response(null, { status: 400, statusText: 'Bad Request' }) + ); + + await expect(sendRequest(TEST_REGTEST_ATTESTOR_APIS[0], 'requestBody')).rejects.toThrow( + new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[0]} was not OK: Bad Request`) + ); + }); + + it('should result in an error when the request fails', async () => { + jest.spyOn(global, 'fetch').mockImplementationOnce(async () => { + throw new Error('Failed to fetch'); + }); + + await expect(sendRequest(TEST_REGTEST_ATTESTOR_APIS[0], 'requestBody')).rejects.toThrow( + new Error(`Failed to fetch`) + ); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 920bab7..8aa3761 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1301,6 +1301,13 @@ "@types/node" "*" kleur "^3.0.3" +"@types/ramda@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.30.1.tgz#316257fec12747bb39a2e921df48a9dcb8c164a9" + integrity sha512-aoyF/ADPL6N+/NXXfhPWF+Qj6w1Cql59m9wX0Gi15uyF+bpzXeLd63HPdiTDE2bmLXfNcVufsDPKmbfOrOzTBA== + dependencies: + types-ramda "^0.30.1" + "@types/stack-utils@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" @@ -4207,6 +4214,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +ramda@^0.30.1: + version "0.30.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.30.1.tgz#7108ac95673062b060025052cd5143ae8fc605bf" + integrity sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw== + randombytes@^2.0.1, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -4747,6 +4759,11 @@ ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-toolbelt@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz#50a25426cfed500d4a09bd1b3afb6f28879edfd5" + integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w== + tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -4808,6 +4825,13 @@ typeforce@^1.11.3, typeforce@^1.11.5, typeforce@^1.18.0: resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== +types-ramda@^0.30.1: + version "0.30.1" + resolved "https://registry.yarnpkg.com/types-ramda/-/types-ramda-0.30.1.tgz#03d255128e3696aeaac76281ca19949e01dddc78" + integrity sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA== + dependencies: + ts-toolbelt "^9.6.0" + typescript-eslint@^7.7.0: version "7.11.0" resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-7.11.0.tgz#7a208fc1d178b3fed58e33ce37150ac6efecf1fb"