Skip to content

Commit

Permalink
chore: merge branch 'main' into feat/dfns-handler
Browse files Browse the repository at this point in the history
  • Loading branch information
Polybius93 committed Dec 2, 2024
2 parents c53929c + 7e8e02a commit 6c33e0a
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 31 deletions.
2 changes: 1 addition & 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": "2.4.10",
"version": "2.4.18",
"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 Down
2 changes: 1 addition & 1 deletion src/constants/ripple.constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { convertStringToHex } from 'xrpl';

export const TRANSACTION_SUCCESS_CODE = 'tesSUCCESS';
export const XRPL_DLCBTC_CURRENCY_HEX = convertStringToHex('dlcBTC').padEnd(40, '0');
export const XRPL_DLCBTC_CURRENCY_HEX = convertStringToHex('iBTC').padEnd(40, '0');
2 changes: 1 addition & 1 deletion src/functions/attestor/attestor-request.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function submitXRPLCheckToCash(
}

export async function getAttestorExtendedGroupPublicKey(coordinatorURL: string): Promise<string> {
return sendGetRequest(`${coordinatorURL}/tss/get-extended-group-publickey`);
return sendGetRequest(`${coordinatorURL}/app/get-extended-group-publickey`);
}

export async function submitFundingPSBT(
Expand Down
35 changes: 26 additions & 9 deletions src/functions/bitcoin/bitcoin-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { P2Ret, P2TROut } from '@scure/btc-signer/payment';
import { TransactionInput } from '@scure/btc-signer/psbt';
import { BIP32Factory, BIP32Interface } from 'bip32';
import { Network } from 'bitcoinjs-lib';
import { Network, address } from 'bitcoinjs-lib';
import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js';
import { Decimal } from 'decimal.js';
import * as ellipticCurveCryptography from 'tiny-secp256k1';
Expand All @@ -39,8 +39,8 @@ const ECDSA_PUBLIC_KEY_LENGTH = 33;
const bip32 = BIP32Factory(ellipticCurveCryptography);

export function getFeeAmount(bitcoinAmount: number, feeBasisPoints: number): number {
const feePercentage = new Decimal(feeBasisPoints).dividedBy(100);
return new Decimal(bitcoinAmount).times(feePercentage.dividedBy(100)).toNumber();
const feePercentage = new Decimal(feeBasisPoints).dividedBy(10000);
return new Decimal(bitcoinAmount).times(feePercentage).trunc().toNumber();
}

/**
Expand Down Expand Up @@ -242,18 +242,35 @@ export async function getBalance(
}

/**
* Gets the Fee Recipient's Address from the Rcipient's Public Key.
* @param feePublicKey - The Fee Recipient's Public Key.
* Validates a Bitcoin Address.
* @param bitcoinAddress
* @param bitcoinNetwork
* @returns A boolean indicating if the Bitcoin Address is valid.
*/
export function isBitcoinAddress(bitcoinAddress: string, bitcoinNetwork: Network): boolean {
try {
return !!address.toOutputScript(bitcoinAddress, bitcoinNetwork);
} catch {
return false;
}
}

/**
* Gets the Fee Recipient's Address from the Recipient's Public Key or Address.
* @param bitcoinFeeRecipient - The Fee Recipient's Public Key or Address.
* @param bitcoinNetwork - The Bitcoin Network to use.
* @returns The Fee Recipient's Address.
*/
export function getFeeRecipientAddressFromPublicKey(
feePublicKey: string,
export function getFeeRecipientAddress(
bitcoinFeeRecipient: string,
bitcoinNetwork: Network
): string {
const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex');
const { address } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork);
if (isBitcoinAddress(bitcoinFeeRecipient, bitcoinNetwork)) return bitcoinFeeRecipient;

const { address } = p2wpkh(Buffer.from(bitcoinFeeRecipient, 'hex'), bitcoinNetwork);

if (!address) throw new Error('Could not create Fee Address from Public Key');

return address;
}

Expand Down
4 changes: 2 additions & 2 deletions src/functions/bitcoin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
finalizeUserInputs,
getBitcoinAddressFromExtendedPublicKey,
getFeeAmount,
getFeeRecipientAddressFromPublicKey,
getFeeRecipientAddress,
getInputIndicesByScript,
} from '../bitcoin/bitcoin-functions.js';
import {
Expand All @@ -27,7 +27,7 @@ export {
finalizeUserInputs,
getFeeAmount,
getBalance,
getFeeRecipientAddressFromPublicKey,
getFeeRecipientAddress,
getInputIndicesByScript,
getBitcoinAddressFromExtendedPublicKey,
};
20 changes: 10 additions & 10 deletions src/functions/bitcoin/psbt-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { reverseBytes } from '../../utilities/index.js';
import {
ecdsaPublicKeyToSchnorr,
getFeeAmount,
getFeeRecipientAddressFromPublicKey,
getFeeRecipientAddress,
getUTXOs,
} from '../bitcoin/bitcoin-functions.js';
import { fetchBitcoinTransaction } from './bitcoin-request-functions.js';
Expand All @@ -25,7 +25,7 @@ import { fetchBitcoinTransaction } from './bitcoin-request-functions.js';
* @param multisigPayment - The Multisig Payment object created from the User's Taproot Public Key, the Attestor's Public Key, and the Unspendable Public Key committed to the Vault's UUID.
* @param depositPayment - The User's Payment object which will be used to fund the Deposit Transaction.
* @param feeRate - The Fee Rate to use for the Transaction.
* @param feePublicKey - The Fee Recipient's Public Key.
* @param feeRecipient - The Fee Recipient's Public Key or Address.
* @param feeBasisPoints - The Fee Basis Points.
* @returns A Funding Transaction.
*/
Expand All @@ -36,7 +36,7 @@ export async function createFundingTransaction(
multisigPayment: P2TROut,
depositPayment: P2Ret | P2TROut,
feeRate: bigint,
feePublicKey: string,
feeRecipient: string,
feeBasisPoints: bigint
): Promise<Transaction> {
const multisigAddress = multisigPayment.address;
Expand All @@ -51,7 +51,7 @@ export async function createFundingTransaction(
throw new Error('Deposit Payment is missing Address');
}

const feeAddress = getFeeRecipientAddressFromPublicKey(feePublicKey, bitcoinNetwork);
const feeAddress = getFeeRecipientAddress(feeRecipient, bitcoinNetwork);
const feeAmount = getFeeAmount(Number(depositAmount), Number(feeBasisPoints));

const userUTXOs = await getUTXOs(depositPayment, bitcoinBlockchainAPIURL);
Expand Down Expand Up @@ -103,7 +103,7 @@ export async function createFundingTransaction(
* @param multisigPayment - The Multisig Payment object created from the User's Taproot Public Key, the Attestor's Public Key, and the Unspendable Public Key committed to the Vault's UUID.
* @param depositPayment - The User's Payment object which will be used to fund the Deposit Transaction.
* @param feeRate - The Fee Rate to use for the Transaction.
* @param feePublicKey - The Fee Recipient's Public Key.
* @param feeRecipient - The Fee Recipient's Public Key or Address.
* @param feeBasisPoints - The Fee Basis Points.
* @returns A Deposit Transaction.
*/
Expand All @@ -115,7 +115,7 @@ export async function createDepositTransaction(
multisigPayment: P2TROut,
depositPayment: P2TROut | P2Ret,
feeRate: bigint,
feePublicKey: string,
feeRecipient: string,
feeBasisPoints: bigint
): Promise<Transaction> {
const multisigAddress = multisigPayment.address;
Expand All @@ -130,7 +130,7 @@ export async function createDepositTransaction(
throw new Error('Deposit Payment is missing Address');
}

const feeAddress = getFeeRecipientAddressFromPublicKey(feePublicKey, bitcoinNetwork);
const feeAddress = getFeeRecipientAddress(feeRecipient, bitcoinNetwork);
const feeAmount = getFeeAmount(Number(depositAmount), Number(feeBasisPoints));

const vaultTransaction = await fetchBitcoinTransaction(vaultTransactionID, bitcoinBlockchainURL);
Expand Down Expand Up @@ -248,7 +248,7 @@ export async function createDepositTransaction(
* @param multisigPayment - The Multisig Payment object created from the User's Taproot Public Key, the Attestor's Public Key, and the Unspendable Public Key committed to the Vault's UUID.
* @param withdrawPayment - The User's Payment object which will be used to receive the withdrawn Bitcoin.
* @param feeRate - The Fee Rate to use for the Transaction.
* @param feePublicKey - The Fee Recipient's Public Key.
* @param feeRecipient - The Fee Recipient's Public Key or Address.
* @param feeBasisPoints - The Fee Basis Points.
* @returns A Withdraw Transaction.
*/
Expand All @@ -260,7 +260,7 @@ export async function createWithdrawTransaction(
multisigPayment: P2TROut,
withdrawPayment: P2Ret | P2TROut,
feeRate: bigint,
feePublicKey: string,
feeRecipient: string,
feeBasisPoints: bigint
): Promise<Transaction> {
const multisigAddress = multisigPayment.address;
Expand Down Expand Up @@ -298,7 +298,7 @@ export async function createWithdrawTransaction(
const remainingAmount =
BigInt(fundingTransaction.vout[fundingTransactionOutputIndex].value) - BigInt(withdrawAmount);

const feeAddress = getFeeRecipientAddressFromPublicKey(feePublicKey, bitcoinNetwork);
const feeAddress = getFeeRecipientAddress(feeRecipient, bitcoinNetwork);
const feeAmount = getFeeAmount(Number(withdrawAmount), Number(feeBasisPoints));

const inputs = [
Expand Down
1 change: 1 addition & 0 deletions src/functions/ripple/ripple.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function decodeURI(URI: string): RawVault {
btcFeeRecipient: URI.slice(332, 398),
taprootPubKey: URI.slice(398, 462),
closingTxId: '', // Deprecated
icyIntegrationAddress: '', // not used in xrpl
};
} catch (error) {
throw new Error(`Could not decode NFT URI: ${error}`);
Expand Down
1 change: 1 addition & 0 deletions src/models/ethereum-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface RawVault {
btcMintFeeBasisPoints: BigNumber;
btcRedeemFeeBasisPoints: BigNumber;
taprootPubKey: string;
icyIntegrationAddress: string;
}

export interface SSPVaultUpdate {
Expand Down
13 changes: 7 additions & 6 deletions src/network-handlers/ripple-handler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { Decimal } from 'decimal.js';
import { BigNumber } from 'ethers';
import {
AutoFillValues,
MultisignatureTransactionResponse,
SignResponse,
XRPLSignatures,
} from 'src/models/ripple.model.js';
import xrpl, {
AccountNFTsRequest,
AccountObject,
Expand All @@ -29,6 +23,12 @@ import {
} from '../functions/ripple/ripple.functions.js';
import { RippleError } from '../models/errors.js';
import { RawVault, SSFVaultUpdate, SSPVaultUpdate } from '../models/ethereum-models.js';
import {
AutoFillValues,
MultisignatureTransactionResponse,
SignResponse,
XRPLSignatures,
} from '../models/ripple.model.js';
import { shiftValue, unshiftValue } from '../utilities/index.js';

function buildDefaultNftVault(): RawVault {
Expand All @@ -47,6 +47,7 @@ function buildDefaultNftVault(): RawVault {
btcMintFeeBasisPoints: BigNumber.from(100),
btcRedeemFeeBasisPoints: BigNumber.from(100),
taprootPubKey: '0'.repeat(64),
icyIntegrationAddress: '',
};
}

Expand Down
3 changes: 3 additions & 0 deletions tests/mocks/ethereum-vault.test.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const TEST_VAULT_1: RawVault = {
btcMintFeeBasisPoints: BigNumber.from('0x64'),
btcRedeemFeeBasisPoints: BigNumber.from('0x64'),
taprootPubKey: '',
icyIntegrationAddress: '',
};

export const TEST_VAULT_2: RawVault = {
Expand All @@ -34,6 +35,7 @@ export const TEST_VAULT_2: RawVault = {
btcMintFeeBasisPoints: BigNumber.from('0x64'),
btcRedeemFeeBasisPoints: BigNumber.from('0x64'),
taprootPubKey: 'dc544c17af0887dfc8ca9936755c9fdef0c79bbc8866cd69bf120c71509742d2',
icyIntegrationAddress: '',
};

export const TEST_VAULT_3: RawVault = {
Expand All @@ -51,4 +53,5 @@ export const TEST_VAULT_3: RawVault = {
btcMintFeeBasisPoints: BigNumber.from('0x64'),
btcRedeemFeeBasisPoints: BigNumber.from('0x64'),
taprootPubKey: 'b362931e3e4cf3cc20f75ae11ff5a4c115ec1548cb5f2c7c48294929f1e8979c',
icyIntegrationAddress: '',
};
104 changes: 103 additions & 1 deletion tests/unit/bitcoin-functions.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { Transaction, p2tr, p2wpkh } from '@scure/btc-signer';
import { regtest, testnet } from 'bitcoinjs-lib/src/networks';
import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks';

import {
createTaprootMultisigPayment,
deriveUnhardenedPublicKey,
ecdsaPublicKeyToSchnorr,
finalizeUserInputs,
getFeeAmount,
getFeeRecipientAddress,
getInputIndicesByScript,
getScriptMatchingOutputFromTransaction,
getUnspendableKeyCommittedToUUID,
} from '../../src/functions/bitcoin/bitcoin-functions';
import { shiftValue, unshiftValue } from '../../src/utilities';
import {
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1,
Expand Down Expand Up @@ -66,6 +69,82 @@ describe('Bitcoin Functions', () => {
});
});

describe('getFeeRecipientAddress', () => {
describe('mainnet', () => {
const network = bitcoin;

it('accepts native segwit (p2wpkh) address', () => {
const address = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4';
expect(getFeeRecipientAddress(address, network)).toBe(address);
});

it('accepts taproot (p2tr) address', () => {
const address = 'bc1qw02rsw9afgp4dsd5n87z5s6rqnf455yhhsnz9f';
expect(getFeeRecipientAddress(address, network)).toBe(address);
});

it('converts public key to native segwit address', () => {
const publicKey = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
const expectedAddress = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4';
expect(getFeeRecipientAddress(publicKey, network)).toBe(expectedAddress);
});
});

describe('testnet', () => {
const network = testnet;

it('accepts native segwit (p2wpkh) address', () => {
const address = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';
expect(getFeeRecipientAddress(address, network)).toBe(address);
});

it('accepts taproot (p2tr) address', () => {
const address = 'tb1qqhy33peyp82mf82fktdtphfmnhtxyhtp6x9hrc';
expect(getFeeRecipientAddress(address, network)).toBe(address);
});

it('converts public key to native segwit address', () => {
const publicKey = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
const expectedAddress = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';
expect(getFeeRecipientAddress(publicKey, network)).toBe(expectedAddress);
});
});

describe('regtest', () => {
const network = regtest;

it('accepts native segwit (p2wpkh) address', () => {
const address = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080';
expect(getFeeRecipientAddress(address, network)).toBe(address);
});

it('accepts taproot (p2tr) address', () => {
const address = 'bcrt1qqhy33peyp82mf82fktdtphfmnhtxyhtpc0u653';
expect(getFeeRecipientAddress(address, network)).toBe(address);
});

it('converts public key to native segwit address', () => {
const publicKey = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
const expectedAddress = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080';
expect(getFeeRecipientAddress(publicKey, network)).toBe(expectedAddress);
});
});

describe('error cases', () => {
it('throws on invalid public key', () => {
const invalidKey = 'invalidPublicKey';
expect(() => getFeeRecipientAddress(invalidKey, bitcoin)).toThrow(
'P2WPKH: invalid publicKey'
);
});

it('throws on invalid address', () => {
const invalidAddress = 'invalidAddress';
expect(() => getFeeRecipientAddress(invalidAddress, bitcoin)).toThrow();
});
});
});

describe('finalizeUserInputs', () => {
it('correctly finalizes inputs given a transaction and a native segwit payment script', () => {
const transaction = Transaction.fromPSBT(
Expand Down Expand Up @@ -180,4 +259,27 @@ describe('Bitcoin Functions', () => {
expect(result).toBeUndefined();
});
});

describe('getFeeAmount', () => {
test('calculates correct fee for whole numbers', () => {
expect(getFeeAmount(1000000, 50)).toBe(5000);
expect(getFeeAmount(1000000, 25)).toBe(2500);
});

test('handles small fee basis points', () => {
expect(getFeeAmount(1000000, 1)).toBe(100);
expect(getFeeAmount(2000000, 1)).toBe(200);
});

test('handles typical fee calculations', () => {
expect(getFeeAmount(1500000, 15)).toBe(2250);
expect(getFeeAmount(2000000, 25)).toBe(5000);
});

test('properly drops decimals', () => {
expect(getFeeAmount(1008584578, 15)).toBe(1512876);
expect(getFeeAmount(1234567, 15)).toBe(1851); // 1234567 * 15 / 10000 = 1851.8505 -> 1851
expect(getFeeAmount(9876543, 23)).toBe(22716); // 9876543 * 23 / 10000 = 22716.0489 -> 22716
});
});
});

0 comments on commit 6c33e0a

Please sign in to comment.