Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14864559
D17701.id52769.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Subscribers
None
D17701.id52769.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Tue, May 20, 20:34 (14 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865889
Default Alt Text
D17701.id52769.diff (33 KB)
Attached To
D17701: [Cashtab] Support one-click redemptions
Event Timeline
Log In to Comment