diff --git a/api/ordinals.ts b/api/ordinals.ts index 3b11bf46..e2470178 100644 --- a/api/ordinals.ts +++ b/api/ordinals.ts @@ -72,12 +72,18 @@ export async function fetchBtcOrdinalsData(btcAddress: string, network: NetworkT return ordinals.sort(sortOrdinalsByConfirmationTime); } -export async function getOrdinalIdFromUtxo(utxo: UTXO) { +export async function getOrdinalIdsFromUtxo(utxo: UTXO): Promise { const ordinalContentUrl = `${XVERSE_INSCRIBE_URL}/v1/inscriptions/utxo/${utxo.txid}/${utxo.vout}`; - const ordinalIds = await axios.get(ordinalContentUrl); - if (ordinalIds.data.length > 0) { - return ordinalIds.data[ordinalIds.data.length - 1]; + const { data: ordinalIds } = await axios.get(ordinalContentUrl); + + return ordinalIds; +} + +export async function getOrdinalIdFromUtxo(utxo: UTXO) { + const ordinalIds = await getOrdinalIdsFromUtxo(utxo); + if (ordinalIds.length > 0) { + return ordinalIds[ordinalIds.length - 1]; } else { return null; } diff --git a/tests/transactions/inscriptionMint.test.ts b/tests/transactions/inscriptionMint.test.ts index 2adc11b0..9e45b461 100644 --- a/tests/transactions/inscriptionMint.test.ts +++ b/tests/transactions/inscriptionMint.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getOrdinalIdsFromUtxo } from '../../api/ordinals'; import xverseInscribeApi from '../../api/xverseInscribe'; import { generateSignedBtcTransaction, selectUtxosForSend } from '../../transactions/btc'; import { @@ -10,6 +11,7 @@ import { import { getBtcPrivateKey } from '../../wallet'; vi.mock('../../api/xverseInscribe'); +vi.mock('../../api/ordinals'); vi.mock('../../transactions/btc'); vi.mock('../../wallet'); @@ -237,6 +239,7 @@ describe('inscriptionMintExecute', () => { vi.mocked(xverseInscribeApi.executeInscriptionOrder).mockResolvedValue({ revealTransactionId: 'revealTxnId', } as any); + vi.mocked(getOrdinalIdsFromUtxo).mockResolvedValue([]); const result = await inscriptionMintExecute({ addressUtxos: [ @@ -264,6 +267,68 @@ describe('inscriptionMintExecute', () => { expect(result).toBe('revealTxnId'); }); + it('fails on no non-ordinal UTXOS', async () => { + vi.mocked(getBtcPrivateKey).mockResolvedValue('dummyPrivateKey'); + vi.mocked(xverseInscribeApi.createInscriptionOrder).mockResolvedValue({ + commitAddress: 'dummyCommitAddress', + commitValue: 1000, + commitValueBreakdown: { + chainFee: 1000, + inscriptionValue: 546, + serviceFee: 2000, + }, + }); + vi.mocked(selectUtxosForSend).mockReturnValue({ + fee: 1100, + change: 1200, + feeRate: 8, + selectedUtxos: [ + { + address: 'dummyAddress', + status: { confirmed: true }, + txid: 'dummyTxId', + vout: 0, + value: 1000, + }, + ], + }); + vi.mocked(generateSignedBtcTransaction).mockResolvedValue({ + hex: 'dummyHex', + } as any); + vi.mocked(xverseInscribeApi.executeInscriptionOrder).mockResolvedValue({ + revealTransactionId: 'revealTxnId', + } as any); + vi.mocked(getOrdinalIdsFromUtxo).mockResolvedValue(['ordinalId']); + + await expect(() => + inscriptionMintExecute({ + addressUtxos: [ + { + address: 'dummyAddress', + status: { confirmed: true }, + txid: 'dummyTxId', + vout: 0, + value: 1000, + }, + ], + contentString: 'dummyContent', + contentType: 'text/plain', + feeRate: 8, + revealAddress: 'dummyRevealAddress', + finalInscriptionValue: 1000, + serviceFee: 5000, + serviceFeeAddress: 'dummyServiceFeeAddress', + accountIndex: 0, + changeAddress: 'dummyChangeAddress', + network: 'Mainnet', + seedPhrase: 'dummySeedPhrase', + }), + ).rejects.toThrowCoreError( + 'Must have at least one non-inscribed UTXO for inscription', + InscriptionErrorCode.NO_NON_ORDINAL_UTXOS, + ); + }); + it('should fail on no UTXOs', async () => { await expect(() => inscriptionMintExecute({ diff --git a/transactions/inscriptionMint.ts b/transactions/inscriptionMint.ts index 37b10425..59ac7297 100644 --- a/transactions/inscriptionMint.ts +++ b/transactions/inscriptionMint.ts @@ -1,6 +1,7 @@ import BigNumber from 'bignumber.js'; import { NetworkType, UTXO } from 'types'; +import { getOrdinalIdsFromUtxo } from '../api/ordinals'; import xverseInscribeApi from '../api/xverseInscribe'; import { CoreError } from '../utils/coreError'; import { getBtcPrivateKey } from '../wallet'; @@ -16,6 +17,7 @@ export enum InscriptionErrorCode { INVALID_CONTENT = 'INVALID_CONTENT', CONTENT_TOO_BIG = 'CONTENT_TOO_BIG', INSCRIPTION_VALUE_TOO_LOW = 'INSCRIPTION_VALUE_TOO_LOW', + NO_NON_ORDINAL_UTXOS = 'NO_NON_ORDINAL_UTXOS', FAILED_TO_FINALIZE = 'FAILED_TO_FINALIZE', SERVER_ERROR = 'SERVER_ERROR', } @@ -169,7 +171,6 @@ export async function inscriptionMintExecute(executeProps: ExecuteProps): Promis finalInscriptionValue, } = executeProps; - // TODO: ensure first UTXO is not inscribed if (!addressUtxos.length) { throw new CoreError('No available UTXOs', InscriptionErrorCode.INSUFFICIENT_FUNDS); } @@ -240,11 +241,30 @@ export async function inscriptionMintExecute(executeProps: ExecuteProps): Promis throw new CoreError('Not enough funds at selected fee rate', InscriptionErrorCode.INSUFFICIENT_FUNDS); } + const selectedOrdinalUtxos = []; + const selectedNonOrdinalUtxos = []; + + for (const utxo of bestUtxoData.selectedUtxos) { + const ordinalIds = await getOrdinalIdsFromUtxo(utxo); + if (ordinalIds.length > 0) { + selectedOrdinalUtxos.push(utxo); + } else { + selectedNonOrdinalUtxos.push(utxo); + } + } + + if (selectedNonOrdinalUtxos.length === 0) { + throw new CoreError( + 'Must have at least one non-inscribed UTXO for inscription', + InscriptionErrorCode.NO_NON_ORDINAL_UTXOS, + ); + } + const commitChainFees = bestUtxoData.fee; const commitTransaction = await generateSignedBtcTransaction( privateKey, - bestUtxoData.selectedUtxos, + [...selectedNonOrdinalUtxos, ...selectedOrdinalUtxos], new BigNumber(commitValue), recipients, changeAddress,