Page MenuHomePhabricator

D17701.id52769.diff
No OneTemporary

D17701.id52769.diff

diff --git a/cashtab/src/components/Etokens/Token/index.tsx b/cashtab/src/components/Etokens/Token/index.tsx
--- a/cashtab/src/components/Etokens/Token/index.tsx
+++ b/cashtab/src/components/Etokens/Token/index.tsx
@@ -154,6 +154,7 @@
} from 'components/Agora/Collection';
import { CashtabCachedTokenInfo } from 'config/CashtabCache';
import { confirmRawTx } from 'components/Send/helpers';
+import { FIRMA } from 'constants/tokens';
const Token: React.FC = () => {
const ContextValue = useContext(WalletContext);
@@ -353,6 +354,7 @@
// But we may not want this to be default for other token types in the future
interface TokenScreenSwitches {
showRedeemXecx: boolean;
+ showRedeemFirma: boolean;
showSend: boolean;
showAirdrop: boolean;
showBurn: boolean;
@@ -364,6 +366,7 @@
}
const switchesOff: TokenScreenSwitches = {
showRedeemXecx: false,
+ showRedeemFirma: false,
showSend: false,
showAirdrop: false,
showBurn: false,
@@ -710,6 +713,10 @@
// If this is the XECX token page, default option is redeeming XECX
// i.e. selling XECX for XEC, 1:1
setSwitches({ ...switchesOff, showRedeemXecx: true });
+ } else if (tokenId === FIRMA.tokenId) {
+ // If this is the Firma token page, default option is redeeming Firma
+ // i.e. selling Firma for XEC at the Firma bid price
+ setSwitches({ ...switchesOff, showRedeemFirma: true });
} else if (isNftChild) {
// Default action is list
setSwitches({ ...switchesOff, showSellNft: true });
@@ -971,10 +978,19 @@
const xecxRedeemError =
isRedeemingXecx && Number(amount) < toXec(appConfig.dustSats);
+ // For Firma redemptions, use 0.01 min
+ const FIRMA_MINIMUM_REDEMPTION = 0.01; // 1 cent
+ const isRedeemingFirma =
+ tokenId === FIRMA.tokenId && switches.showRedeemFirma;
+ const firmaRedeemError =
+ isRedeemingFirma && Number(amount) < FIRMA_MINIMUM_REDEMPTION;
+
setAgoraPartialTokenQtyError(
isValidAmountOrErrorMsg === true
? xecxRedeemError
? `Cannot redeem less than 5.46 XECX`
+ : firmaRedeemError
+ ? `Cannot redeem less than ${FIRMA_MINIMUM_REDEMPTION} FIRMA`
: false
: isValidAmountOrErrorMsg,
);
@@ -1646,6 +1662,133 @@
}
};
+ const getFirmaPartialUnitPrice = (firmaPartial: AgoraPartial) => {
+ const offeredAtoms = firmaPartial.offeredAtoms();
+ const acceptPriceSats = firmaPartial.askedSats(offeredAtoms);
+ const acceptPriceXec = toXec(Number(acceptPriceSats));
+
+ // Convert atoms to FIRMA
+ const minAcceptedTokens = decimalizeTokenAmount(
+ offeredAtoms.toString(),
+ decimals as SlpDecimals,
+ );
+
+ // Get the unit price
+ // For FIRMA, we expect this to be > 1 XEC
+ // So, limit to 2 decimal places
+ const actualPricePerToken = new BigNumber(acceptPriceXec)
+ .div(minAcceptedTokens)
+ .dp(2);
+
+ // Return price as a number
+ return actualPricePerToken.toNumber();
+ };
+
+ /**
+ * Firma redemption has a dynamic price which must be fetched from an API endpoint
+ * We want to sell for as close as we can get to the bid price (due to discrete values
+ * of agora offers, it is unlikely we can get the exact bid price)
+ */
+ const previewFirmaPartial = async () => {
+ // Get the bid price
+
+ let firmaBidPrice;
+ try {
+ const firmaBidPriceResp = await fetch(`https://firma.cash/api/bid`);
+ const firmaBidPriceJson = await firmaBidPriceResp.json();
+ firmaBidPrice = firmaBidPriceJson.bid;
+ console.info(`FIRMA buys at: ${firmaBidPrice} XEC`);
+ } catch (err) {
+ console.error(`Error fetching FIRMA bid price`, err);
+ toast.error(`Error determining FIRMA bid price: ${err}`);
+ return;
+ }
+
+ const priceNanoSatsPerDecimalizedToken =
+ xecToNanoSatoshis(firmaBidPrice);
+
+ // Adjust for atoms
+ // e.g. a 9-decimal token, the user sets the the price for 1.000000000 tokens
+ // but you must create the offer with priceNanoSatsPerToken for 1 atom
+ // i.e. 0.000000001 token
+ let priceNanoSatsPerAtom =
+ BigInt(priceNanoSatsPerDecimalizedToken) /
+ BigInt(Math.pow(10, decimals as SlpDecimals));
+
+ // Convert formData list qty (a decimalized token qty) to BigInt token sats
+ const userSuggestedOfferedTokens = BigInt(
+ undecimalizeTokenAmount(
+ agoraPartialTokenQty,
+ decimals as SlpDecimals,
+ ),
+ );
+
+ let firmaPartial;
+ try {
+ const firmaPartialParams = {
+ tokenId: tokenId,
+ // We cannot render the Token screen until tokenType is defined
+ tokenType: (tokenType as TokenType).number,
+ // We cannot render the Token screen until protocol is defined
+ tokenProtocol: protocol as 'ALP' | 'SLP',
+ offeredAtoms: userSuggestedOfferedTokens,
+ priceNanoSatsPerAtom: priceNanoSatsPerAtom,
+ makerPk: pk,
+ minAcceptedAtoms: userSuggestedOfferedTokens,
+ };
+ firmaPartial = await agora.selectParams(firmaPartialParams);
+
+ let actualPrice = getFirmaPartialUnitPrice(firmaPartial);
+ if (actualPrice > firmaBidPrice) {
+ // Keep making firmaPartials until we have one that is acceptable
+ // Reduce price by 50 XEC at a time
+ // In practice, this takes 2 or 3 iterations
+ // The quanta are such that we get "the next tick down", we won't
+ // skip it
+ const NANOSATS_PER_ATOM_REDUCTION_PER_ITERATION = 500000000n;
+
+ // Counter to prevent infinite loop
+ let attempts = 0;
+ const MAX_ATTEMPTS = 5;
+ while (actualPrice > firmaBidPrice) {
+ attempts += 1;
+ priceNanoSatsPerAtom -=
+ NANOSATS_PER_ATOM_REDUCTION_PER_ITERATION;
+ // This time we only update the price, we do not need to update locktime
+ firmaPartial = await agora.selectParams({
+ ...firmaPartialParams,
+ priceNanoSatsPerAtom,
+ });
+ actualPrice = getFirmaPartialUnitPrice(firmaPartial);
+ // loop repeats until actualPrice <= firmaBidPrice
+ if (attempts > MAX_ATTEMPTS) {
+ // If we try more than 5 times, there is probably something wrong
+ // or weird about this specific request
+ // Maybe some quantities are difficult to price properly
+ toast.error(
+ 'Unable to determine FIRMA redemption price',
+ );
+ return;
+ }
+ }
+ }
+ return setPreviewedAgoraPartial(firmaPartial);
+ } catch (err) {
+ // We can run into errors trying to create an agora partial
+ // Most of these are prevented by validation in Cashtab
+ // However some are a bit testier, e.g.
+ // "Parameters cannot be represented in Script"
+ // "minAcceptedTokens too small, got truncated to 0"
+ // Catch and give a generic error
+ console.error(`Error creating AgoraPartial`, err);
+ toast.error(
+ `Unable to create Agora offer with these parameters, try increasing the min buy.`,
+ );
+ // Do not show the preview modal
+ return;
+ }
+ };
+
/**
* Note that listing ALP tokens is simpler than listing SLP tokens
* Thanks to EMPP, can be done in a single tx, instead of the required
@@ -2159,7 +2302,8 @@
/>
)}
{showConfirmListPartialSlp &&
- formData.tokenListPrice !== '' &&
+ (formData.tokenListPrice !== '' ||
+ tokenId === FIRMA.tokenId) &&
previewedAgoraPartial !== null && (
<Modal
title={`List ${tokenTicker}?`}
@@ -2233,14 +2377,16 @@
{getAgoraPartialActualPrice()}
</AgoraPreviewCol>
</AgoraPreviewRow>
- <AgoraPreviewRow>
- <AgoraPreviewLabel>
- Target price:{' '}
- </AgoraPreviewLabel>
- <AgoraPreviewCol>
- {getAgoraPartialTargetPriceXec()}
- </AgoraPreviewCol>
- </AgoraPreviewRow>
+ {tokenId !== FIRMA.tokenId && (
+ <AgoraPreviewRow>
+ <AgoraPreviewLabel>
+ Target price:{' '}
+ </AgoraPreviewLabel>
+ <AgoraPreviewCol>
+ {getAgoraPartialTargetPriceXec()}
+ </AgoraPreviewCol>
+ </AgoraPreviewRow>
+ )}
</AgoraPreviewTable>
<AgoraPreviewParagraph>
If actual price is not close to target
@@ -2690,6 +2836,95 @@
)}
</>
)}
+ {tokenId === FIRMA.tokenId && (
+ <>
+ <SwitchHolder>
+ <Switch
+ name="Toggle Redeem FIRMA"
+ on="🤳"
+ off="🤳"
+ checked={
+ switches.showRedeemFirma
+ }
+ handleToggle={() => {
+ // We turn everything else off, whether we are turning this one on or off
+ setSwitches({
+ ...switchesOff,
+ showRedeemFirma:
+ !switches.showRedeemFirma,
+ });
+ }}
+ />
+ <SwitchLabel>
+ Redeem {tokenName}
+ </SwitchLabel>
+ </SwitchHolder>
+ {switches.showRedeemFirma && (
+ <>
+ <SendTokenFormRow>
+ <InputRow>
+ <Slider
+ name={
+ 'agoraPartialTokenQty'
+ }
+ label={`Offered qty`}
+ value={
+ agoraPartialTokenQty
+ }
+ handleSlide={
+ handleTokenOfferedSlide
+ }
+ error={
+ agoraPartialTokenQtyError
+ }
+ min={0}
+ max={
+ tokenBalance
+ }
+ step={parseFloat(
+ `1e-${decimals}`,
+ )}
+ allowTypedInput
+ />
+ </InputRow>
+ </SendTokenFormRow>
+
+ {!tokenListPriceError &&
+ formData.tokenListPrice !==
+ '' &&
+ formData.tokenListPrice !==
+ null &&
+ fiatPrice !== null && (
+ <ListPricePreview title="Token List Price">
+ {getAgoraPartialPricePreview()}
+ </ListPricePreview>
+ )}
+ <SendTokenFormRow>
+ <PrimaryButton
+ style={{
+ marginTop:
+ '12px',
+ }}
+ disabled={
+ apiError ||
+ agoraPartialTokenQtyError !==
+ false ||
+ agoraPartialTokenQty ===
+ '0' ||
+ agoraPartialTokenQty ===
+ ''
+ }
+ onClick={
+ previewFirmaPartial
+ }
+ >
+ Redeem FIRMA for XEC
+ </PrimaryButton>
+ </SendTokenFormRow>
+ </>
+ )}
+ </>
+ )}
{isNftChild ? (
<>
<SwitchHolder>
diff --git a/cashtab/src/components/Etokens/__tests__/TokenActions.test.js b/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
--- a/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
+++ b/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
@@ -39,6 +39,7 @@
import { Agora } from 'ecash-agora';
import { token as tokenConfig } from 'config/token';
import { explorer } from 'config/explorer';
+import { FIRMA } from 'constants/tokens';
describe('<Token /> available actions rendered', () => {
const ecc = new Ecc();
@@ -2068,4 +2069,153 @@
),
).toBeInTheDocument();
});
+ it('We can redeem 1 Firma for $1 of XEC using a workflow unique to Firma', async () => {
+ // Mock Math.random()
+ jest.spyOn(global.Math, 'random').mockReturnValue(0.5); // set a fixed value
+
+ // Mock a bid price
+ when(fetch)
+ .calledWith(`https://firma.cash/api/bid`)
+ .mockResolvedValue({
+ json: () => Promise.resolve({ bid: 40000.0 }),
+ });
+
+ // FIRMA offer tx
+ const offerHex =
+ '020000000288bb5c0d60e11b4038b00af152f9792fa954571ffdd2413a85f1c26bfd930c25010000006441243d709268b45b7917eb446ed0cb447fa71eec05977b7b558cb2d7cbae3b1b8bc190810e03b84ceb037b7295bca76e76ad83d48a8f8d9f891de93995adca244d4121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffffef76d01776229a95c45696cf68f2f98c8332d0c53e3f24e73fd9c6deaf792618030000006441c9656b6789947fe5fe369072e95fb3f39a1b21f37b6a1602ee609840ce5b77c55d0b5d1e455020629d28c4791fa705b535e0dd0a4563e130bdcbb5129b5a57ef4121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffff040000000000000000866a504b41475230075041525449414c0000e253000000000000360000000000000040b9fe7f000000002099c53f031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02d37534c5032000453454e44f0cb08302c4bbc665b6241592b19fd37ec5d632f323e9ab14fdb75d57f94870302a08601000000a0bb0d000000220200000000000017a914d269ef0be66e9b689bee7a071d08cc0a7151b32a8722020000000000001976a91400549451e5c22b18686cacdf34dce649e5ec3be288ac83300f00000000001976a91400549451e5c22b18686cacdf34dce649e5ec3be288ac00000000';
+ const offerTxid =
+ '322da86c0fd6b008298bc21f6a344647d225f3ed20ff597860d7e9ab9f5428f7';
+ mockedChronik.setBroadcastTx(offerHex, offerTxid);
+
+ // Make sure it's cached
+ mockedChronik.setTx(FIRMA.tx.txid, FIRMA.tx);
+ mockedChronik.setToken(FIRMA.tokenId, FIRMA.token);
+
+ // Mock response for agora select params check
+ // Note
+ // We obtain EXPECTED_OFFER_P2SH by adding
+ // console.log(toHex(shaRmd160(agoraScript.bytecode)));
+ // to ecash-agora lib and running this test
+ // Note that Date() and Math.random() must be mocked to keep this deterministic
+ const EXPECTED_OFFER_P2SH = '28967de39bdb1af326e5cb2ffecf1f320dedfb04';
+ // Note we have to create a second partial to get an acceptable price
+ const EXPECTED_SECOND_P2SH = 'd269ef0be66e9b689bee7a071d08cc0a7151b32a';
+
+ // We mock no existing utxos
+ mockedChronik.setUtxosByScript('p2sh', EXPECTED_OFFER_P2SH, []);
+ mockedChronik.setUtxosByScript('p2sh', EXPECTED_SECOND_P2SH, []);
+
+ // Note that we cannot use mockedAgora to avoid agoraQueryErrors, as we need a proper
+ // agora object to build the partial
+ const agora = new Agora(mockedChronik);
+
+ render(
+ <CashtabTestWrapper
+ chronik={mockedChronik}
+ ecc={ecc}
+ agora={agora}
+ route={`/send-token/${FIRMA.tokenId}`}
+ />,
+ );
+
+ const { tokenName } = FIRMA.token.genesisInfo;
+
+ // Wait for element to get token info and load
+ expect(
+ (await screen.findAllByText(new RegExp(tokenName)))[0],
+ ).toBeInTheDocument();
+
+ // XECX token icon is rendered
+ expect(
+ screen.getByAltText(`icon for ${FIRMA.tokenId}`),
+ ).toBeInTheDocument();
+
+ // Token actions are available
+ expect(screen.getByTitle('Token Actions')).toBeInTheDocument();
+
+ // On load, default action for FIRMA is to redeem it
+ expect(screen.getByTitle('Toggle Redeem FIRMA')).toBeEnabled();
+
+ // The redeem button is disabled on load
+ const redeemButton = await screen.findByRole('button', {
+ name: /Redeem FIRMA for XEC/,
+ });
+ expect(redeemButton).toBeDisabled();
+
+ // We do not see a price input
+ expect(
+ screen.queryByPlaceholderText('Enter list price (per token)'),
+ ).not.toBeInTheDocument();
+
+ // We do not see a min qty input
+ expect(
+ screen.queryByPlaceholderText('Min qty'),
+ ).not.toBeInTheDocument();
+
+ // Enter amount to redeem
+ await userEvent.type(
+ screen.getByPlaceholderText('Offered qty'),
+ '0.009',
+ );
+
+ // This is below firma min redemption so we get an error
+ expect(
+ screen.getByText('Cannot redeem less than 0.01 FIRMA'),
+ ).toBeInTheDocument();
+
+ // The redeem button is still disabled
+ expect(redeemButton).toBeDisabled();
+
+ // OK we redeem more than dust
+ await userEvent.clear(screen.getByPlaceholderText('Offered qty'));
+
+ await userEvent.type(screen.getByPlaceholderText('Offered qty'), '10');
+
+ expect(screen.getByPlaceholderText('Offered qty')).toHaveValue('10');
+
+ // The redeem button is now enabled
+ expect(redeemButton).toBeEnabled();
+
+ // Redeem
+ await userEvent.click(redeemButton);
+
+ screen.debug(null, Infinity);
+
+ // Async as we must wait for multiple partials
+ expect(await screen.findByText('List FIRMA?')).toBeInTheDocument();
+ expect(
+ screen.getByText('Create the following sell offer?'),
+ ).toBeInTheDocument();
+
+ // Offered qty (actual, calculated from AgoraOffer)
+ const actualOfferedQty = '10.0000';
+ // We see this two times bc it is also behind the modal
+ expect(screen.getAllByText(actualOfferedQty)).toHaveLength(2);
+ // Actual price calculated from AgoraOffer
+ const actualPricePerTokenForMinBuy = '39,766.67 XEC';
+ // We see the price once; it is not previewed as we need to calculate it before we
+ // show the modal
+ expect(
+ screen.getByText(actualPricePerTokenForMinBuy),
+ ).toBeInTheDocument();
+
+ // We can cancel and not create this listing
+ await userEvent.click(screen.getByText('Cancel'));
+
+ // The confirmation modal is gone
+ expect(screen.queryByText('List FIRMA?')).not.toBeInTheDocument();
+
+ // We change our mind and list it
+ await userEvent.click(redeemButton);
+
+ expect(await screen.findByText('List FIRMA?')).toBeInTheDocument();
+ await userEvent.click(screen.getByText('OK'));
+
+ // We see the expected toast notification for the successful listing tx
+ expect(
+ await screen.findByText(
+ `${actualOfferedQty} Firma listed for ${actualPricePerTokenForMinBuy} per token`,
+ ),
+ ).toBeInTheDocument();
+ });
});
diff --git a/cashtab/src/components/Etokens/fixtures/mocks.js b/cashtab/src/components/Etokens/fixtures/mocks.js
--- a/cashtab/src/components/Etokens/fixtures/mocks.js
+++ b/cashtab/src/components/Etokens/fixtures/mocks.js
@@ -4,6 +4,7 @@
import { fromHex } from 'ecash-lib';
import { tokenMockXecx } from 'components/Agora/fixtures/mocks';
+import { FIRMA } from 'constants/tokens';
/**
* Etokens/fixtures/mocks.js
@@ -78,6 +79,28 @@
},
path: 1899,
},
+ // FIRMA
+ {
+ outpoint: {
+ txid: '250c93fd6bc2f1853a41d2fd1f5754a92f79f952f10ab038401be1600d5cbb88',
+ outIdx: 1,
+ },
+ blockHeight: 836452,
+ isCoinbase: false,
+ sats: 546n,
+ isFinal: true,
+ token: {
+ tokenId: FIRMA.tokenId,
+ tokenType: {
+ protocol: 'ALP',
+ type: 'ALP_TOKEN_TYPE_STANDARD',
+ number: 0,
+ },
+ atoms: 1000000n,
+ isMintBaton: false,
+ },
+ path: 1899,
+ },
{
outpoint: {
txid: '74a8598eed00672e211553a69e22334128199883fe79eb4ad64f9c0b7909735c',
@@ -2297,6 +2320,31 @@
],
};
+// FIRMA
+export const firmaMocks = {
+ ...FIRMA,
+ utxos: [
+ {
+ ...MOCK_TOKEN_UTXO,
+ token: {
+ ...MOCK_TOKEN_UTXO.token,
+ tokenId: FIRMA.tokenId,
+ atoms: 62500_0000n,
+ },
+ },
+ // Include a mint baton as it is variable supply
+ {
+ ...MOCK_TOKEN_UTXO,
+ token: {
+ ...MOCK_TOKEN_UTXO.token,
+ tokenId: FIRMA.tokenId,
+ atoms: 0n,
+ isMintBaton: true,
+ },
+ },
+ ],
+};
+
export const supportedTokens = [
slp1FixedMocks,
slp1VarMocks,
@@ -2305,6 +2353,7 @@
slp1NftChildMocks,
alpMocks,
xecxMocks,
+ firmaMocks,
];
/**
diff --git a/cashtab/src/constants/tokens.ts b/cashtab/src/constants/tokens.ts
new file mode 100644
--- /dev/null
+++ b/cashtab/src/constants/tokens.ts
@@ -0,0 +1,179 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { Tx, TokenInfo } from 'chronik-client';
+
+interface TokenConst {
+ tokenId: string;
+ token: TokenInfo;
+ tx: Tx;
+}
+
+export const FIRMA: TokenConst = {
+ tokenId: '0387947fd575db4fb19a3e322f635dec37fd192b5941625b66bc4b2c3008cbf0',
+ token: {
+ tokenId:
+ '0387947fd575db4fb19a3e322f635dec37fd192b5941625b66bc4b2c3008cbf0',
+ tokenType: {
+ protocol: 'ALP',
+ type: 'ALP_TOKEN_TYPE_STANDARD',
+ number: 0,
+ },
+ timeFirstSeen: 1740005671,
+ genesisInfo: {
+ tokenTicker: 'FIRMA',
+ tokenName: 'Firma',
+ url: 'firma.cash',
+ decimals: 4,
+ data: '',
+ authPubkey:
+ '03fba49912622cf8bb5b3729b1b5da3e72c6b57d369c8647f6cc7c6cbed510d105',
+ },
+ block: {
+ height: 884824,
+ hash: '0000000000000000042420f0d007398b85c2a4b02d894575de72441c055099fc',
+ timestamp: 1740006660,
+ },
+ },
+ tx: {
+ txid: '0387947fd575db4fb19a3e322f635dec37fd192b5941625b66bc4b2c3008cbf0',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: 'dc13a52129537222797ca324f4f0a130e7ae0f9c57bb36e53ff13c3510b88496',
+ outIdx: 0,
+ },
+ inputScript:
+ '41a753034ede0327bf8188d14f5407c774c61d78208a317a83bd30475adb14ba0760990fe1f25a4ea885bf0c0ffcae23122148541ac5a89397ebfffb4840f78fbb412103fba49912622cf8bb5b3729b1b5da3e72c6b57d369c8647f6cc7c6cbed510d105',
+ sats: 4200n,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a914cf76d8e334b149cb49ad1f95de339c3e6e9ed54188ac',
+ },
+ ],
+ outputs: [
+ {
+ sats: 0n,
+ outputScript:
+ '6a504c50534c5032000747454e45534953054649524d41054669726d610a6669726d612e63617368002103fba49912622cf8bb5b3729b1b5da3e72c6b57d369c8647f6cc7c6cbed510d1050401807c814a000003',
+ },
+ {
+ sats: 546n,
+ outputScript:
+ '76a914cf76d8e334b149cb49ad1f95de339c3e6e9ed54188ac',
+ token: {
+ tokenId:
+ '0387947fd575db4fb19a3e322f635dec37fd192b5941625b66bc4b2c3008cbf0',
+ tokenType: {
+ protocol: 'ALP',
+ type: 'ALP_TOKEN_TYPE_STANDARD',
+ number: 0,
+ },
+ atoms: 1250000000n,
+ isMintBaton: false,
+ entryIdx: 0,
+ },
+ spentBy: {
+ txid: '83b7d403d5b7ae295ff1e2a66ef220ed6ae190b019df4822643bea545518734a',
+ outIdx: 0,
+ },
+ },
+ {
+ sats: 546n,
+ outputScript:
+ '76a914cf76d8e334b149cb49ad1f95de339c3e6e9ed54188ac',
+ token: {
+ tokenId:
+ '0387947fd575db4fb19a3e322f635dec37fd192b5941625b66bc4b2c3008cbf0',
+ tokenType: {
+ protocol: 'ALP',
+ type: 'ALP_TOKEN_TYPE_STANDARD',
+ number: 0,
+ },
+ atoms: 0n,
+ isMintBaton: true,
+ entryIdx: 0,
+ },
+ },
+ {
+ sats: 546n,
+ outputScript:
+ '76a91438d2e1501a485814e2849552093bb0588ed9acbb88ac',
+ token: {
+ tokenId:
+ '0387947fd575db4fb19a3e322f635dec37fd192b5941625b66bc4b2c3008cbf0',
+ tokenType: {
+ protocol: 'ALP',
+ type: 'ALP_TOKEN_TYPE_STANDARD',
+ number: 0,
+ },
+ atoms: 0n,
+ isMintBaton: true,
+ entryIdx: 0,
+ },
+ spentBy: {
+ txid: 'd6fafa6977a24133789fcdec7922c14cfaf3120072ae82e21abcc3a68b6ed6c7',
+ outIdx: 0,
+ },
+ },
+ {
+ sats: 546n,
+ outputScript:
+ '76a914a66cd958eed093c209643e62a7f56fc9eb46622c88ac',
+ token: {
+ tokenId:
+ '0387947fd575db4fb19a3e322f635dec37fd192b5941625b66bc4b2c3008cbf0',
+ tokenType: {
+ protocol: 'ALP',
+ type: 'ALP_TOKEN_TYPE_STANDARD',
+ number: 0,
+ },
+ atoms: 0n,
+ isMintBaton: true,
+ entryIdx: 0,
+ },
+ },
+ {
+ sats: 1602n,
+ outputScript:
+ '76a914cf76d8e334b149cb49ad1f95de339c3e6e9ed54188ac',
+ spentBy: {
+ txid: '83b7d403d5b7ae295ff1e2a66ef220ed6ae190b019df4822643bea545518734a',
+ outIdx: 1,
+ },
+ },
+ ],
+ lockTime: 0,
+ timeFirstSeen: 1740005671,
+ size: 414,
+ isCoinbase: false,
+ tokenEntries: [
+ {
+ tokenId:
+ '0387947fd575db4fb19a3e322f635dec37fd192b5941625b66bc4b2c3008cbf0',
+ tokenType: {
+ protocol: 'ALP',
+ type: 'ALP_TOKEN_TYPE_STANDARD',
+ number: 0,
+ },
+ txType: 'GENESIS',
+ isInvalid: false,
+ burnSummary: '',
+ failedColorings: [],
+ actualBurnAtoms: 0n,
+ intentionalBurnAtoms: 0n,
+ burnsMintBatons: false,
+ },
+ ],
+ tokenFailedParsings: [],
+ tokenStatus: 'TOKEN_STATUS_NORMAL',
+ isFinal: true,
+ block: {
+ height: 884824,
+ hash: '0000000000000000042420f0d007398b85c2a4b02d894575de72441c055099fc',
+ timestamp: 1740006660,
+ },
+ },
+};
diff --git a/cashtab/src/transactions/index.js b/cashtab/src/transactions/index.js
--- a/cashtab/src/transactions/index.js
+++ b/cashtab/src/transactions/index.js
@@ -188,6 +188,7 @@
// Otherwise, broadcast the tx
const txSer = tx.ser();
const hex = toHex(txSer);
+ console.log(`hex`, hex);
// Will throw error on node failing to broadcast tx
// e.g. 'txn-mempool-conflict (code 18)'
const response = await chronik.broadcastTx(hex, isBurn);

File Metadata

Mime Type
text/plain
Expires
Tue, May 20, 20:34 (9 h, 40 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865889
Default Alt Text
D17701.id52769.diff (33 KB)

Event Timeline