Skip to content

Commit

Permalink
feat: add brc 20 transfer hook to core (#199)
Browse files Browse the repository at this point in the history
* feat: implement required calls to inscribe service

* feat: do full BRC-20 transfer in 1 call

* fix: import/export names

* fix: fee calc and utxo select

* fix: type issue

* feat: Add all brc-20 api calls

* feat: implement 1 step transfer generator

* chore: add btc functions for brc-20 transfer

* chore: add brc-20 tests

* oopsie

* feat: added mint functions and exponential backoff for retries

* chore: add unit tests for mint

* feat: add react dependencies

* feat: add estimate hook

* chore: make bignumber a peer dependency

* feat: add brc-20 transfer execute hook

* doc: add docstring for transfer hook

* chore: tie bignumber to v 9

* fix: make arguments an object instead of multiple arguments

* Change args to obj instead of multiple args

* chore: move props to object

* chore: extract validation to method

* chore: move validation to function in other hook

* chore: make complete flag better

* chore: use useCallback instead of effect

* fix: the callback dependencies for executing transfer

* export hooks

* build kick off

* fix: fee calc resulting in fee higher than input

* fix: tests

* allow undefined UTXOs in fee estimate until loaded

* fee hook, don't show error code if not initialised

* Add missing return types and add comment

* Add transfer finalize call

* AAdd finalize call to transfer script and fix tests

* implement CoreError

* Fix coreerror import path

* fix issues form merge

* fix: tests

* fix: extract finalize retry to utils

* Fix callback deps

* fix: remove retry since all txns broadcast in finalise

* review feedback: move error codes to function

* export CoreError

* fix: remove isInitialised output and return fees if insufficient funds

* fix: import

* remove execute validation in favor of validation in transaction section

* remove unused error codes and fix tests

* fix import path

* add skipInitialFetch to brc20 fee estimate hook and fix docstrings

* redo PR review fix
  • Loading branch information
victorkirov authored Sep 7, 2023
1 parent 2120d9c commit 3036ee5
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 147 deletions.
96 changes: 28 additions & 68 deletions hooks/brc20/useBrc20TransferExecute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,38 @@ import { CoreError } from '../../utils/coreError';

import { BRC20ErrorCode, ExecuteTransferProgressCodes, brc20TransferExecute } from '../../transactions/brc20';

export enum ErrorCode {
INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS',
INVALID_TICK = 'INVALID_TICK',
INVALID_AMOUNT = 'INVALID_AMOUNT',
INVALID_FEE_RATE = 'INVALID_FEE_RATE',
BROADCAST_FAILED = 'BROADCAST_FAILED',
SERVER_ERROR = 'SERVER_ERROR',
}

type Props = {
/** The seed phrase of the wallet. */
seedPhrase: string;

/** The account index of the seed phrase to use. */
accountIndex: number;

/** The UTXOs in the bitcoin address which will be used for payment. */
addressUtxos: UTXO[];

/** The 4 letter BRC-20 token name. */
tick: string;

/** The amount of the BRC-20 token to transfer. */
amount: number;
revealAddress: string;
changeAddress: string;
recipientAddress: string;
feeRate: number;
network: NetworkType;
};

const validateProps = (props: Props) => {
const { addressUtxos, tick, amount, feeRate } = props;
/** The address where the balance of the BRC-20 token lives. This is usually the ordinals address. */
revealAddress: string;

if (!addressUtxos.length) {
return ErrorCode.INSUFFICIENT_FUNDS;
}
/** The address where change SATS will be sent to. Should be the Bitcoin address of the wallet. */
changeAddress: string;

if (tick.length !== 4) {
return ErrorCode.INVALID_TICK;
}
/** The address where the BRC-20 tokens will be sent to. */
recipientAddress: string;

if (amount <= 0) {
return ErrorCode.INVALID_AMOUNT;
}
/** The desired fee rate for the transactions. */
feeRate: number;

if (feeRate <= 0) {
return ErrorCode.INVALID_FEE_RATE;
}
return undefined;
/** The network to broadcast the transactions on (Mainnet or Testnet). */
network: NetworkType;
};

/**
*
* @param seedPhrase - The seed phrase of the wallet
* @param accountIndex - The account index of the seed phrase to use
* @param addressUtxos - The UTXOs in the bitcoin address which will be used for payment
* @param tick - The 4 letter BRC-20 token name
* @param amount - The amount of the BRC-20 token to transfer
* @param revealAddress - The address where the balance of the BRC-20 token lives. This is usually the ordinals address.
* @param changeAddress - The address where change SATS will be sent to. Should be the Bitcoin address of the wallet.
* @param recipientAddress - The address where the BRC-20 tokens will be sent to.
* @param feeRate - The desired fee rate for the transactions
* @param network - The network to broadcast the transactions on (Mainnet or Testnet)
* @returns
*/
const useBrc20TransferExecute = (props: Props) => {
const {
seedPhrase,
Expand All @@ -80,10 +55,10 @@ const useBrc20TransferExecute = (props: Props) => {
const [revealTransactionId, setRevealTransactionId] = useState<string | undefined>();
const [transferTransactionId, setTransferTransactionId] = useState<string | undefined>();
const [progress, setProgress] = useState<ExecuteTransferProgressCodes | undefined>();
const [errorCode, setErrorCode] = useState<ErrorCode | undefined>();
const [errorCode, setErrorCode] = useState<BRC20ErrorCode | undefined>();

const executeTransfer = useCallback(() => {
if (running || !!transferTransactionId) return;
if (running) return;

const innerProps = {
seedPhrase,
Expand All @@ -98,16 +73,11 @@ const useBrc20TransferExecute = (props: Props) => {
network,
};

const validationErrorCode = validateProps(innerProps);
setErrorCode(validationErrorCode);

if (validationErrorCode) {
return;
}

// if we get to here, that means that the transfer is valid and we can try to execute it but we don't want to
// be able to accidentally execute it again if something goes wrong, so we set the running flag
setRunning(true);
setErrorCode(undefined);
setProgress(undefined);

const runTransfer = async () => {
try {
Expand All @@ -133,22 +103,13 @@ const useBrc20TransferExecute = (props: Props) => {
}
} while (!done);
} catch (e) {
let finalErrorCode: string | undefined;
if (CoreError.isCoreError(e)) {
finalErrorCode = e.code;
}

switch (finalErrorCode) {
case BRC20ErrorCode.FAILED_TO_FINALIZE:
setErrorCode(ErrorCode.BROADCAST_FAILED);
break;
case BRC20ErrorCode.INSUFFICIENT_FUNDS:
setErrorCode(ErrorCode.INSUFFICIENT_FUNDS);
break;
default:
setErrorCode(ErrorCode.SERVER_ERROR);
break;
setErrorCode(e.code as BRC20ErrorCode);
} else {
setErrorCode(BRC20ErrorCode.SERVER_ERROR);
}
} finally {
setRunning(false);
}
};

Expand All @@ -165,7 +126,6 @@ const useBrc20TransferExecute = (props: Props) => {
feeRate,
network,
running,
transferTransactionId,
]);

return {
Expand Down
114 changes: 48 additions & 66 deletions hooks/brc20/useBrc20TransferFees.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { useEffect, useState } from 'react';

import { UTXO } from 'types';
import { brc20TransferEstimateFees } from '../../transactions/brc20';
import { BRC20ErrorCode, brc20TransferEstimateFees } from '../../transactions/brc20';
import { CoreError } from '../../utils/coreError';

const DUMMY_UTXO = {
address: '',
txid: '1234567890123456789012345678901234567890123456789012345678901234',
vout: 0,
status: { confirmed: true },
value: 100e8,
};

type CommitValueBreakdown = {
commitChainFee: number;
Expand All @@ -11,86 +20,43 @@ type CommitValueBreakdown = {
transferUtxoValue: number;
};

export enum ErrorCode {
UTXOS_MISSING = 'UTXOS_MISSING',
INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS',
INVALID_TICK = 'INVALID_TICK',
INVALID_AMOUNT = 'INVALID_AMOUNT',
INVALID_FEE_RATE = 'INVALID_FEE_RATE',
SERVER_ERROR = 'SERVER_ERROR',
}

type Props = {
/** The UTXOs in the bitcoin address which will be used for payment. */
addressUtxos: UTXO[] | undefined;
tick: string;
amount: number;
feeRate: number;
revealAddress: string;
};

const validateProps = (props: Props) => {
const { addressUtxos, tick, amount, feeRate } = props;

if (!addressUtxos) {
return ErrorCode.UTXOS_MISSING;
}

if (!addressUtxos.length) {
return ErrorCode.INSUFFICIENT_FUNDS;
}
/** The 4 letter BRC-20 token name. */
tick: string;

if (tick.length !== 4) {
return ErrorCode.INVALID_TICK;
}
/** The amount of the BRC-20 token to transfer. */
amount: number;

if (amount <= 0) {
return ErrorCode.INVALID_AMOUNT;
}
/** The desired fee rate for the transactions. */
feeRate: number;

if (feeRate <= 0) {
return ErrorCode.INVALID_FEE_RATE;
}
/** The address where the balance of the BRC-20 token lives. This is usually the ordinals address. */
revealAddress: string;

return null;
/** If true, the initial fetch will be skipped. */
skipInitialFetch?: boolean;
};

/**
* Estimates the fees for a BRC-20 1-step transfer
* @param addressUtxos - The UTXOs in the bitcoin address which will be used for payment
* @param tick - The 4 letter BRC-20 token name
* @param amount - The amount of the BRC-20 token to transfer
* @param feeRate - The desired fee rate for the transactions
* @param revealAddress - The address where the balance of the BRC-20 token lives. This is usually the ordinals address.
*/
const useBrc20TransferFees = (props: Props) => {
const { addressUtxos, tick, amount, feeRate, revealAddress } = props;
const { addressUtxos = [], tick, amount, feeRate, revealAddress, skipInitialFetch = false } = props;
const [commitValue, setCommitValue] = useState<number | undefined>();
const [commitValueBreakdown, setCommitValueBreakdown] = useState<CommitValueBreakdown | undefined>();
const [isInitialised, setIsInitialised] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [errorCode, setErrorCode] = useState<ErrorCode | undefined>();

useEffect(() => {
const validationErrorCode = validateProps(props);

if (validationErrorCode) {
setErrorCode(validationErrorCode);
const [errorCode, setErrorCode] = useState<BRC20ErrorCode | undefined>();

if (validationErrorCode !== ErrorCode.UTXOS_MISSING) {
setIsInitialised(true);
}

return;
}
const [isInitialised, setIsInitialised] = useState(false);

setIsInitialised(true);
useEffect(() => {
setIsLoading(true);
setErrorCode(undefined);

const runEstimate = async () => {
try {
const result = await brc20TransferEstimateFees({
addressUtxos: addressUtxos!,
addressUtxos,
tick,
amount,
revealAddress,
Expand All @@ -99,25 +65,41 @@ const useBrc20TransferFees = (props: Props) => {
setCommitValue(result.commitValue);
setCommitValueBreakdown(result.valueBreakdown);
} catch (e) {
if (e.message === 'Not enough funds at selected fee rate') {
setErrorCode(ErrorCode.INSUFFICIENT_FUNDS);
if (CoreError.isCoreError(e) && (e.code ?? '') in BRC20ErrorCode) {
setErrorCode(e.code as BRC20ErrorCode);

// if there are not enough funds, we get the fee again with a fictitious UTXO to show what the fee would be
if (e.code === BRC20ErrorCode.INSUFFICIENT_FUNDS) {
const result = await brc20TransferEstimateFees({
addressUtxos: [DUMMY_UTXO],
tick,
amount,
revealAddress,
feeRate,
});
setCommitValue(result.commitValue);
setCommitValueBreakdown(result.valueBreakdown);
}
} else {
setErrorCode(ErrorCode.SERVER_ERROR);
setErrorCode(BRC20ErrorCode.SERVER_ERROR);
}
}

setIsLoading(false);
};

runEstimate();
if (!skipInitialFetch || isInitialised) {
runEstimate();
}

setIsInitialised(true);
}, [addressUtxos, tick, amount, revealAddress, feeRate]);

return {
commitValue,
commitValueBreakdown,
isLoading,
errorCode: isInitialised ? errorCode : undefined,
isInitialised,
errorCode,
};
};

Expand Down
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export * from './hooks';
export * from './ledger';
export * from './transactions';
export * from './types';
export { CoreError } from './utils/coreError';
export * from './wallet';
Loading

0 comments on commit 3036ee5

Please sign in to comment.