diff --git a/package.json b/package.json index 99e321b..086975a 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,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 +69,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..f2f646a --- /dev/null +++ b/src/functions/attestor/attestor-request.functions.ts @@ -0,0 +1,83 @@ +import { isEmpty, join } from 'ramda'; +import { + FundingTXAttestorInfo, + WithdrawDepositTXAttestorInfo, +} from 'src/models/attestor.models.js'; +import { AttestorError } from 'src/models/errors.js'; + +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 AttestorError(`Attestor Response ${url} was not OK: ${response.statusText}`); + } + return true; +} + +export async function submitFundingPSBT( + attestorRootURLs: string[], + fundingTXAttestorInfo: FundingTXAttestorInfo +): Promise { + if (isEmpty(attestorRootURLs)) { + throw new AttestorError('No Attestor URLs provided'); + } + + const fundingEndpoints = 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 => + 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: + ${join('|', attestorResponses)}` + ); + } +} + +export async function submitWithdrawDepositPSBT( + attestorRootURLs: string[], + withdrawDepositTXAttestorInfo: WithdrawDepositTXAttestorInfo +): Promise { + if (isEmpty(attestorRootURLs)) { + throw new AttestorError('No Attestor URLs provided'); + } + + const depositWithdrawEndpoints = 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 => + 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: + ${join('|', attestorResponses)}` + ); + } +} 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/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/tests/unit/attestor-request-functions.test.ts b/tests/unit/attestor-request-functions.test.ts new file mode 100644 index 0000000..78e221b --- /dev/null +++ b/tests/unit/attestor-request-functions.test.ts @@ -0,0 +1,51 @@ +import * as attestorRequestFunctions from '../../src/functions/attestor/attestor-request.functions.js'; +import { submitFundingPSBT } from '../../src/functions/attestor/attestor-request.functions.js'; + +describe('Attestor ', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('submitFundingPSBT', () => { + it('should not throw an error if all requests were succesful', async () => { + jest + .spyOn(attestorRequestFunctions, 'sendRequest') + .mockImplementationOnce(async () => true) + .mockImplementationOnce(async () => true) + .mockImplementationOnce(async () => true); + + await expect( + submitFundingPSBT( + ['http://localhost:3000', 'http://localhost:4000', 'http://localhost:5000'], + { + vaultUUID: 'vaultUUID', + fundingPSBT: 'fundingPSBT', + userEthereumAddress: 'userEthereumAddress', + attestorChainID: 'evm-arbitrum', + userBitcoinTaprootPublicKey: 'userBitcoinTaprootPublicKey', + } + ) + ).resolves.not.toThrow(); + }); + + it('should not throw an error if not all requests were succesful', async () => { + jest + .spyOn(attestorRequestFunctions, 'sendRequest') + .mockImplementationOnce(async () => true) + .mockImplementationOnce(async () => true) + .mockImplementationOnce(async () => false); + + await expect( + submitFundingPSBT( + ['http://localhost:3000', 'http://localhost:4000', 'http://localhost:5000'], + { + vaultUUID: 'vaultUUID', + fundingPSBT: 'fundingPSBT', + userEthereumAddress: 'userEthereumAddress', + attestorChainID: 'evm-arbitrum', + userBitcoinTaprootPublicKey: 'userBitcoinTaprootPublicKey', + } + ) + ).resolves.not.toThrow(); + }); + }); +}); 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"