Skip to content

Commit 0c2aa43

Browse files
authored
Issuance Form Edits (#166)
* fix: improve issuance review screens and hide max button Issue Supply: - Hide Max button (max is 2^63 - currentSupply, not meaningful) - Update review to use supply_normalized from verbose API response - Add formatAmount for proper "After Issuance" display Lock Supply: - Simplify review to use supply_normalized from verbose API Lock Description: - Remove redundant warning from review (form already has warning + confirmation) Also: - Add supply and supply_normalized fields to ComposeAssetInfo type - Update test helper with supply fields * feat: add random numeric asset generator and fix quantity bug 1. Random numeric asset generator: - Add "# Random" button to AssetNameInput when creating new assets - Generates valid numeric asset (A{number} in range 26^12+1 to 256^8) - Only shows for regular assets, not subassets - Free to create (no 0.5 XCP cost) 2. Fix quantity double-conversion bug: - When returning from review, quantity was already converted to satoshis - Form would convert again on re-submit, causing massive inflation - Now detects and normalizes quantities when initializing from formData 3. Reduce description textarea to 1 row: - Asset descriptions are typically short - Single row is cleaner for the form * style: change random asset button to 'A123' with cursor pointer * style: add refresh icon to A123 button * style: remove title attribute from A123 button * fix: smaller icon (size-2) and add htmlFor to label for focus
1 parent 668edfb commit 0c2aa43

8 files changed

Lines changed: 109 additions & 29 deletions

File tree

src/components/ui/inputs/asset-name-input.tsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { forwardRef, useEffect, useState, useRef } from "react";
22
import { Field, Label, Description, Input } from "@headlessui/react";
3+
import { FiRefreshCw } from "@/components/icons";
34
import { fetchAssetDetails } from "@/utils/blockchain/counterparty/api";
45
import { useWallet } from "@/contexts/wallet-context";
56
import { validateAssetName } from "@/utils/validation/asset";
@@ -19,6 +20,35 @@ interface AssetNameInputProps {
1920
isSubasset?: boolean;
2021
parentAsset?: string;
2122
autoFocus?: boolean;
23+
/** Show button to generate random numeric asset name (only for non-subassets) */
24+
showRandomNumeric?: boolean;
25+
}
26+
27+
/**
28+
* Generate a random numeric asset name in the valid range.
29+
* Numeric assets are in format A{number} where number is between 26^12+1 and 256^8.
30+
* Based on Counterparty validation: lower_bound = 26**12 + 1, upper_bound = 256**8
31+
*/
32+
function generateRandomNumericAsset(): string {
33+
// Valid range: 26^12 + 1 to 256^8 (inclusive)
34+
const min = BigInt(26) ** BigInt(12) + BigInt(1); // 95,428,956,661,682,177
35+
const max = BigInt(256) ** BigInt(8); // 18,446,744,073,709,551,616
36+
37+
// Generate random BigInt in range
38+
const range = max - min + BigInt(1); // +1 because upper bound is inclusive
39+
40+
// Generate random bytes and convert to BigInt
41+
const randomBytes = new Uint8Array(8);
42+
crypto.getRandomValues(randomBytes);
43+
let randomValue = BigInt(0);
44+
for (let i = 0; i < 8; i++) {
45+
randomValue = (randomValue << BigInt(8)) | BigInt(randomBytes[i]);
46+
}
47+
48+
// Scale to our range and add min
49+
const value = min + (randomValue % range);
50+
51+
return `A${value.toString()}`;
2252
}
2353

2454
// The component uses the validation from utils internally
@@ -41,6 +71,7 @@ export const AssetNameInput = forwardRef<HTMLInputElement, AssetNameInputProps>(
4171
isSubasset = false,
4272
parentAsset = "",
4373
autoFocus = false,
74+
showRandomNumeric = false,
4475
},
4576
ref
4677
) => {
@@ -247,11 +278,29 @@ export const AssetNameInput = forwardRef<HTMLInputElement, AssetNameInputProps>(
247278
displayText = helpText;
248279
}
249280

281+
const handleRandomNumeric = () => {
282+
const randomAsset = generateRandomNumericAsset();
283+
onChange(randomAsset);
284+
};
285+
250286
return (
251287
<Field>
252-
<Label className="text-sm font-medium text-gray-700">
253-
{label} {required && <span className="text-red-500">*</span>}
254-
</Label>
288+
<div className="flex items-center justify-between">
289+
<Label htmlFor={name} className="text-sm font-medium text-gray-700">
290+
{label} {required && <span className="text-red-500">*</span>}
291+
</Label>
292+
{showRandomNumeric && !isSubasset && (
293+
<button
294+
type="button"
295+
onClick={handleRandomNumeric}
296+
disabled={disabled}
297+
className="text-xs text-blue-600 hover:text-blue-800 disabled:text-gray-400 cursor-pointer font-mono flex items-center gap-1"
298+
>
299+
<FiRefreshCw className="size-2" />
300+
A123
301+
</button>
302+
)}
303+
</div>
255304
<div className="relative">
256305
<Input
257306
ref={(el: HTMLInputElement | null) => {

src/pages/compose/issuance/form.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,20 @@ export function IssuanceForm({
4545
// Form status
4646
const { pending } = useFormStatus();
4747

48-
// Form state
48+
// Form state - normalize quantity from satoshis if returning from review
49+
const getInitialAmount = (): string => {
50+
if (!initialFormData?.quantity) return "";
51+
const qty = initialFormData.quantity.toString();
52+
// If divisible was set and quantity looks like satoshis, convert back
53+
if (initialFormData?.divisible && Number(qty) >= 100000000) {
54+
return toBigNumber(qty).dividedBy(100000000).toString();
55+
}
56+
return qty;
57+
};
58+
4959
const [assetName, setAssetName] = useState(initialFormData?.asset || (initialParentAsset ? `${initialParentAsset}.` : ""));
5060
const [isAssetNameValid, setIsAssetNameValid] = useState(false);
51-
const [amount, setAmount] = useState(initialFormData?.quantity?.toString() || "");
61+
const [amount, setAmount] = useState(getInitialAmount());
5262
const [isDivisible, setIsDivisible] = useState(initialFormData?.divisible ?? false);
5363
const [isLocked, setIsLocked] = useState(initialFormData?.lock ?? false);
5464
const [description, setDescription] = useState(initialFormData?.description || "");
@@ -196,6 +206,7 @@ export function IssuanceForm({
196206
parentAsset={initialParentAsset}
197207
disabled={pending}
198208
showHelpText={showHelpText}
209+
showRandomNumeric={!initialParentAsset}
199210
required
200211
autoFocus
201212
/>
@@ -268,7 +279,7 @@ export function IssuanceForm({
268279
value={description}
269280
onChange={setDescription}
270281
label="Description"
271-
rows={4}
282+
rows={1}
272283
disabled={pending}
273284
showHelpText={showHelpText}
274285
helpText="A textual description for the asset."

src/pages/compose/issuance/issue-supply/form.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useState, useEffect } from "react";
22
import { useFormStatus } from "react-dom";
33
import { ComposerForm } from "@/components/composer/composer-form";
44
import { Spinner } from "@/components/ui/spinner";
@@ -39,11 +39,33 @@ export function IssueSupplyForm({
3939
// Form status
4040
const { pending } = useFormStatus();
4141

42-
// Form state
43-
const [quantity, setQuantity] = useState(initialFormData?.quantity?.toString() || "");
42+
// Form state - normalize quantity from satoshis if returning from review
43+
const getInitialQuantity = (): string => {
44+
if (!initialFormData?.quantity) return "";
45+
const qty = initialFormData.quantity.toString();
46+
// If we have asset info and it's divisible, the stored quantity is in satoshis
47+
// We need to convert back to user-friendly format
48+
// Check if the value looks like it was converted (large number for divisible)
49+
if (assetInfo?.divisible && Number(qty) >= 100000000) {
50+
return toBigNumber(qty).dividedBy(100000000).toString();
51+
}
52+
return qty;
53+
};
54+
55+
const [quantity, setQuantity] = useState(getInitialQuantity());
4456
const [lock, setLock] = useState(initialFormData?.lock || false);
4557
const [, setError] = useState<string | null>(null);
4658

59+
// Update quantity if assetInfo loads after initial render (for proper normalization)
60+
useEffect(() => {
61+
if (assetInfo && initialFormData?.quantity) {
62+
const qty = initialFormData.quantity.toString();
63+
if (assetInfo.divisible && Number(qty) >= 100000000) {
64+
setQuantity(toBigNumber(qty).dividedBy(100000000).toString());
65+
}
66+
}
67+
}, [assetInfo, initialFormData?.quantity]);
68+
4769
// Calculate maximum issuable amount
4870
const calculateMaxAmount = (): string => {
4971
if (!assetInfo) return "0";
@@ -149,8 +171,9 @@ export function IssueSupplyForm({
149171
maxAmount={calculateMaxAmount()}
150172
label="Amount"
151173
name="quantity_display"
152-
description={`Amount of ${asset} to issue (max: ${calculateMaxAmount()})`}
153-
disableMaxButton={false}
174+
description={`Enter the amount of ${asset} to issue`}
175+
disableMaxButton={true}
176+
isDivisible={assetInfo?.divisible ?? false}
154177
/>
155178

156179
<CheckboxInput

src/pages/compose/issuance/issue-supply/review.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ReviewScreen } from "@/components/screens/review-screen";
2+
import { formatAmount } from "@/utils/format";
23

34
interface ReviewIssuanceIssueSupplyProps {
45
apiResponse: any;
@@ -22,9 +23,10 @@ export function ReviewIssuanceIssueSupply({
2223
const issuedQuantity = result.params.quantity_normalized ?? result.params.quantity;
2324

2425
// Calculate new total supply from normalized values
25-
const newTotalSupply = (
26-
Number(currentSupply) + Number(issuedQuantity)
27-
).toString();
26+
const newTotalSupply = formatAmount({
27+
value: Number(currentSupply) + Number(issuedQuantity),
28+
minimumFractionDigits: 0,
29+
});
2830

2931
const customFields = [
3032
{ label: "Asset", value: result.params.asset },

src/pages/compose/issuance/lock-description/review.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,11 @@ export function ReviewLockDescription({
1919

2020
const customFields = [
2121
{ label: "Asset", value: result.params.asset },
22-
{
23-
label: "Action",
22+
{
23+
label: "Action",
2424
value: "Lock Description",
2525
className: "text-red-600 font-medium"
2626
},
27-
{
28-
label: "Warning",
29-
value: "This permanently prevents future description changes",
30-
className: "text-red-600 text-sm"
31-
},
3227
];
3328

3429
return (

src/pages/compose/issuance/lock-supply/review.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { ReviewScreen } from "@/components/screens/review-screen";
2-
import { formatAssetQuantity } from "@/utils/format";
32

43
/**
54
* Props for the ReviewIssuanceLockSupply component.
65
*/
76
interface ReviewIssuanceLockSupplyProps {
8-
apiResponse: any; // Consider typing this more strictly based on your API response shape
7+
apiResponse: any;
98
onSign: () => void;
109
onBack: () => void;
1110
error: string | null;
12-
isSigning: boolean; // Passed from useActionState in Composer
11+
isSigning: boolean;
1312
}
1413

1514
/**
@@ -25,12 +24,9 @@ export function ReviewIssuanceLockSupply({
2524
isSigning
2625
}: ReviewIssuanceLockSupplyProps) {
2726
const { result } = apiResponse;
28-
const isDivisible = result.params.asset_info.divisible;
2927

30-
31-
const currentSupply = result.params.asset_info.supply
32-
? formatAssetQuantity(result.params.asset_info.supply, isDivisible)
33-
: "0";
28+
// Use normalized supply from verbose API response (handles divisibility correctly)
29+
const currentSupply = result.params.asset_info?.supply_normalized ?? "0";
3430

3531
const customFields = [
3632
{ label: "Asset", value: result.params.asset },

src/utils/blockchain/counterparty/__tests__/helpers/composeTestHelpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export const createMockComposeResult = (overrides?: Partial<ComposeResult>): Com
5050
divisible: true,
5151
locked: false,
5252
owner: mockAddress,
53+
supply: '100000000',
54+
supply_normalized: '1.00000000',
5355
},
5456
quantity_normalized: '1.00000000',
5557
},

src/utils/blockchain/counterparty/compose.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export interface ComposeAssetInfo {
6161
divisible: boolean;
6262
locked: boolean;
6363
owner: string;
64+
supply?: string;
65+
supply_normalized?: string;
6466
}
6567

6668
export interface ComposeParams {

0 commit comments

Comments
 (0)