Skip to content

Commit

Permalink
feat: remove unnecessary attestor handler (#20)
Browse files Browse the repository at this point in the history
* feat: replace attestor handler with attestor functions

* feat: modify attestor request functions, add tests

* revert: 1.0.23 package update

* 2.0.0
  • Loading branch information
Polybius93 authored Jul 19, 2024
1 parent ae4120c commit 5e7b1d5
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 79 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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": {
Expand Down Expand Up @@ -61,13 +62,15 @@
"@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",
"decimal.js": "^10.4.3",
"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"
}
Expand Down
75 changes: 0 additions & 75 deletions src/attestor-handlers/attestor-handler.ts

This file was deleted.

65 changes: 65 additions & 0 deletions src/functions/attestor/attestor-request.functions.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
await submitPSBT(attestorRootURLs, withdrawDepositTXAttestorInfo, '/app/withdraw', info => ({
uuid: info.vaultUUID,
wd_psbt: info.withdrawDepositPSBT,
}));
}

export async function submitPSBT<T>(
attestorRootURLs: string[],
transactionInfo: T,
endpointPath: string,
transformBody: (transactionInfo: T) => object
): Promise<void> {
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)}`
);
}
};
4 changes: 4 additions & 0 deletions src/functions/attestor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
submitFundingPSBT,
submitWithdrawDepositPSBT,
} from '../attestor/attestor-request.functions.js';
11 changes: 11 additions & 0 deletions src/functions/request/request.functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export async function sendRequest(url: string, body: string): Promise<void> {
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}`);
}
}
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,6 +11,5 @@ export {
SoftwareWalletDLCHandler,
EthereumHandler,
ReadOnlyEthereumHandler,
AttestorHandler,
ProofOfReserveHandler,
};
2 changes: 1 addition & 1 deletion src/models/attestor.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ export interface FundingTXAttestorInfo {

export interface WithdrawDepositTXAttestorInfo {
vaultUUID: string;
depositWithdrawPSBT: string;
withdrawDepositPSBT: string;
}
139 changes: 139 additions & 0 deletions tests/unit/attestor-request-function.test.ts
Original file line number Diff line number Diff line change
@@ -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')
);
});
});
});
43 changes: 43 additions & 0 deletions tests/unit/request-functions.test.ts
Original file line number Diff line number Diff line change
@@ -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`)
);
});
});
});
Loading

0 comments on commit 5e7b1d5

Please sign in to comment.