Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
92f114c
fix seetings btn and slippage input
SorinC6 Sep 17, 2025
e01aa40
force to one decimal
SorinC6 Sep 17, 2025
51c2684
force one decimal
SorinC6 Sep 17, 2025
a5ab5b2
fixes
SorinC6 Sep 18, 2025
49b4ff1
update dispatch
SorinC6 Sep 18, 2025
8b1d67d
add constant
SorinC6 Sep 18, 2025
2b11d4a
add cancel order tx review implementation
SorinC6 Sep 19, 2025
5e434c8
fix condition
SorinC6 Sep 19, 2025
f0ef502
add refresh, fix state update
SorinC6 Sep 23, 2025
6f431f4
refactor context reducer
SorinC6 Sep 26, 2025
e6bac2c
fix string
Nebyt Sep 29, 2025
1a37aa1
Merge branch 'develop' into sorin/YOEXT-2223/swap-fixes
vsubhuman Sep 29, 2025
25f0ae2
fix route
SorinC6 Sep 29, 2025
fe2c251
fix typo
SorinC6 Sep 29, 2025
dc61f33
fix format
SorinC6 Sep 29, 2025
edceb06
removed console.log
SorinC6 Sep 29, 2025
3bddbfe
remove console.log
SorinC6 Sep 29, 2025
2814a34
fix parsing number
SorinC6 Sep 29, 2025
30f8458
fix format
SorinC6 Sep 29, 2025
c697a88
fix imports
SorinC6 Sep 29, 2025
051ed79
Merge branch 'develop' into sorin/YOEXT-2223/swap-fixes
Nebyt Sep 30, 2025
875efa3
refactor cip30 implementation
SorinC6 Sep 30, 2025
64d63f3
update cancel order
SorinC6 Sep 30, 2025
e66c033
fix asset input
SorinC6 Sep 30, 2025
d48615b
fix crash on review inputs
SorinC6 Sep 30, 2025
87bf1ee
Update packages/yoroi-extension/app/UI/features/swap-new/common/compo…
SorinC6 Sep 30, 2025
7579257
Update packages/yoroi-extension/app/UI/features/swap-new/common/compo…
SorinC6 Sep 30, 2025
f0fc8ca
removed unused code
SorinC6 Sep 30, 2025
42eb0d1
add const
SorinC6 Sep 30, 2025
d6f8d4a
refactor and extract to fnc
SorinC6 Sep 30, 2025
1799ae5
add string
SorinC6 Sep 30, 2025
47df64d
fix format
SorinC6 Sep 30, 2025
f68c0ed
Merge branch 'develop' into sorin/YOEXT-2223/swap-fixes
SorinC6 Sep 30, 2025
4e08a7a
Merge branch 'sorin/YOEXT-2223/swap-fixes' into fix/YOEXT-2247/swap
SorinC6 Sep 30, 2025
c796e13
fix close bracket
SorinC6 Sep 30, 2025
d9cae40
Merge branch 'develop' into fix/YOEXT-2247/swap
SorinC6 Sep 30, 2025
ae9ef5d
Merge branch 'develop' into fix/YOEXT-2247/swap
Nebyt Oct 3, 2025
0c9f353
update address on receiver
SorinC6 Oct 3, 2025
ff988a8
remove console.log
SorinC6 Oct 3, 2025
e75af14
Merge branch 'develop' into fix/YOEXT-2247/swap
Nebyt Oct 3, 2025
e1bf1f0
fix format, update package
SorinC6 Oct 6, 2025
b2fc2fa
Merge branch 'develop' into fix/YOEXT-2247/swap
Nebyt Oct 6, 2025
d9c7de8
Merge branch 'develop' into fix/YOEXT-2247/swap
SorinC6 Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,27 +73,18 @@ export const AssetInput: React.FC<AssetInputProps> = ({ direction, onAssetSelect
}

const assetInputName = React.useMemo(() => {
if (direction === ASSET_DIRECTION_IN) {
return tokenInputInfo?.ticker ? tokenInputInfo?.name : primaryTokenInfo.name;
if (direction === ASSET_DIRECTION_OUT && !touched) {
return 'Select token';
}
if (direction === ASSET_DIRECTION_OUT) {
if (!touched) {
return 'Select token';
}
return tokenInput.tokenId === '.' ? primaryTokenInfo.name : (tokenInputInfo?.ticker ?? tokenInputInfo?.name);
}
return undefined;
}, [direction, tokenInputInfo]);

return tokenInput.tokenId === '.' || tokenInput.tokenId === ''
? primaryTokenInfo.name
: (tokenInputInfo?.ticker ?? tokenInputInfo?.name);
}, [direction, tokenInputInfo, swapForm.tokenInInput.tokenId, swapForm.tokenOutInput.tokenId]);

const AssetIdForIcon = React.useMemo(() => {
if (direction === ASSET_DIRECTION_IN) {
return tokenInput.tokenId ?? tokenInputInfo?.id;
}
if (direction === ASSET_DIRECTION_OUT) {
return tokenInput.tokenId === '.' ? primaryTokenInfo.id : tokenInputInfo?.id;
}
return undefined;
}, [direction, tokenInputInfo, tokenInput]);
return tokenInput.tokenId === '.' || tokenInput.tokenId === '' ? '.' : tokenInputInfo?.id;
}, [direction, tokenInputInfo, swapForm.tokenInInput.tokenId, swapForm.tokenOutInput.tokenId]);

const focusInput = () => {
if (inputRef?.current) {
Expand Down Expand Up @@ -220,6 +211,7 @@ const Wrapper = styled(Box, {
}`,
backgroundColor: direction === ASSET_DIRECTION_IN ? 'transparent' : theme.palette.ds.bg_color_contrast_min,
height: '132px',
width: '506px',

'&:hover': {
borderColor: !hasError && theme.palette.ds.el_gray_max,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ export const SettingsModalContent = () => {
};

const applyChanges = async () => {
await swapForm.action({ type: SwapActionType.ProtocolSelected, value: routingPreference });
await swapForm.action({
type: SwapActionType.ProtocolSelected,
value: routingPreference === DEX_ROUTING.BOTH ? DEX_ROUTING.AUTO : routingPreference,
});
await swapForm.action({ type: SwapActionType.SlippageInputChanged, value: Number(selectedSlippage) });
await swapManager.assignSettings({
slippage: Number(selectedSlippage),
routingPreference,
routingPreference: routingPreference === DEX_ROUTING.BOTH ? DEX_ROUTING.AUTO : routingPreference,
});
closeModal();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const MARKET_ORDER = 'market';
export const LIMIT_ORDER = 'limit';

export const undefinedToken: Portfolio.Token.Id = '.unknown';
export const USDA_TOKEN_ID = 'fe7c786ab321f41c654ef6c1af7b3250a613c24e4213e0425a7ae456.55534441';

export const DEX_ROUTING = {
AUTO: 'auto',
Expand Down
180 changes: 107 additions & 73 deletions packages/yoroi-extension/app/UI/features/swap-new/common/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,121 @@
import { RustModule } from '../../../../api/ada/lib/cardanoCrypto/rustLoader';
export const normalizeTokenId = (id?: string | null) => (id === '' ? '.' : id);

export const useGetInputs = (walletUtxos: any[]) => {
const getInputs = async (amounts: { [tokenId: string]: string }) => {
try {
const tokenId = Object.keys(amounts)[0];
if (tokenId === undefined) {
throw new Error('No tokenId provided in amounts');
}
const requiredAmount = BigInt(Number(amounts[tokenId]));

const matching = walletUtxos
.map(utxo => {
const token = utxo.output.tokens.find(t => {
return tokenId === '.' ? t.Token.Metadata.ticker === 'ADA' : t.Token.Identifier === tokenId;
});

return token
? {
utxo,
amount: BigInt(token.TokenList.Amount),
}
: null;
})
.filter(Boolean)
.sort((a, b) => (a!.amount > b!.amount ? -1 : 1)) as {
utxo: any;
amount: bigint;
}[];

const selected: any[] = [];
let total = BigInt(0);

for (const { utxo, amount } of matching) {
selected.push(utxo);
total += amount;
if (total >= requiredAmount) break;
}
type WalletUtxo = {
address: string; // raw address bytes (hex)
output: {
Transaction: { Hash: string }; // 64-hex
UtxoTransactionOutput: { OutputIndex: number };
tokens?: Array<{
Token: {
Identifier: string; // '.' for ADA, or `${policyId}.${assetNameHex}`
Metadata: { ticker?: string; policyId?: string; assetName?: string };
};
TokenList: { Amount: string }; // decimal string
}>;
inlineDatumCborHex?: string;
datumHashHex?: string;
scriptRefCborHex?: string;
};
};

if (total < requiredAmount) {
throw new Error('Not enough balance');
const isHex = (s: string) => /^[0-9a-f]*$/i.test(s);
const isHex64 = (s: string) => /^[0-9a-f]{64}$/i.test(s);
const hexToBytes = (hex: string): Uint8Array => {
if (!isHex(hex) || hex.length % 2 !== 0) throw new Error('Invalid hex');
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
return out;
};
const bytesToHex = (bytes: Uint8Array): string => {
let hex = '';
// @ts-ignore
for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, '0');
return hex;
};
const looksLikeTUSO = (hex: string) =>
typeof hex === 'string' && /^[0-9a-f]+$/i.test(hex) && hex.toLowerCase().startsWith('82825820'); // 82 [input,output] / 82 [hash,index] / 58 20 (32-byte hash)

export const useGetInputs = (walletUtxos: WalletUtxo[]) => {
// encode one UTxO -> CBOR hex TUSO
const encodeUtxo = (u: WalletUtxo): string => {
const { WalletV4 } = RustModule;

const input = WalletV4.TransactionInput.new(
WalletV4.TransactionHash.from_hex(u.output.Transaction.Hash),
u.output.UtxoTransactionOutput.OutputIndex
);

const addr = WalletV4.Address.from_bytes(hexToBytes(u.address));

const value = WalletV4.Value.new(WalletV4.BigNum.from_str('0'));
const ma = WalletV4.MultiAsset.new();
let lovelaceSet = false;

for (const t of u.output.tokens ?? []) {
const amt = WalletV4.BigNum.from_str(t.TokenList.Amount);
const isAda = t.Token.Identifier === '.' || (t.Token.Metadata?.ticker ?? '').toUpperCase() === 'ADA';

if (isAda) {
value.set_coin(amt);
lovelaceSet = true;
} else {
const policyId = t.Token.Metadata?.policyId;
const assetNameHex = t.Token.Metadata?.assetName;
if (!policyId || !assetNameHex) continue;
const policy = WalletV4.ScriptHash.from_hex(policyId);
const assets = ma.get(policy) ?? WalletV4.Assets.new();
assets.insert(WalletV4.AssetName.new(hexToBytes(assetNameHex)), amt);
ma.insert(policy, assets);
}
}

const inputs = await Promise.all(
selected.map(async u => {
const txHash = u.output.Transaction.Hash;
const index = u.output.UtxoTransactionOutput.OutputIndex;

const receiver = await RustModule.WalletV4.Address.from_bytes(Buffer.from(u.address, 'hex')).to_bech32();

const input = RustModule.WalletV4.TransactionInput.new(RustModule.WalletV4.TransactionHash.from_hex(txHash), index);

const value = RustModule.WalletV4.Value.new(RustModule.WalletV4.BigNum.from_str('0'));

for (const token of u.output.tokens) {
const amt = RustModule.WalletV4.BigNum.from_str(token.TokenList.Amount);

if (token.Token.Metadata.ticker === 'ADA') {
value.set_coin(amt);
} else {
const policyId = RustModule.WalletV4.ScriptHash.from_hex(token.Token.Metadata.policyId);
const assetName = RustModule.WalletV4.AssetName.new(Buffer.from(token.Token.Metadata.assetName, 'hex'));
if (!lovelaceSet) value.set_coin(WalletV4.BigNum.from_str('0'));
if (ma.len() > 0) value.set_multiasset(ma);

const multiasset = value.multiasset() || RustModule.WalletV4.MultiAsset.new();
const assets = multiasset.get(policyId) || RustModule.WalletV4.Assets.new();
assets.insert(assetName, amt);
multiasset.insert(policyId, assets);
value.set_multiasset(multiasset);
}
}
const out = WalletV4.TransactionOutput.new(addr, value);

const output = RustModule.WalletV4.TransactionOutput.new(RustModule.WalletV4.Address.from_bech32(receiver), value);
if (u.output.inlineDatumCborHex) {
out.set_datum(WalletV4.Datum.new_data(WalletV4.PlutusData.from_bytes(hexToBytes(u.output.inlineDatumCborHex))));
} else if (u.output.datumHashHex) {
out.set_datum(WalletV4.Datum.new_data_hash(WalletV4.DataHash.from_bytes(hexToBytes(u.output.datumHashHex))));
}
if (u.output.scriptRefCborHex) {
out.set_script_ref(WalletV4.ScriptRef.from_bytes(hexToBytes(u.output.scriptRefCborHex)));
}

const utxo = RustModule.WalletV4.TransactionUnspentOutput.new(input, output);
return Buffer.from(utxo.to_bytes()).toString('hex');
})
);
const tuso = WalletV4.TransactionUnspentOutput.new(input, out);
const hex = bytesToHex(tuso.to_bytes());
if (!looksLikeTUSO(hex)) throw new Error(`Not a full TransactionUnspentOutput: ${hex.slice(0, 10)}…`);
return hex;
};

return inputs;
} catch {
console.warn('Failed to get inputs');
return [];
// dedupe + validate by (txHash#index)
const sanitize = (list: WalletUtxo[]) => {
const seen = new Set<string>();
const out: WalletUtxo[] = [];
for (const u of list) {
const txh = u.output?.Transaction?.Hash;
const idx = u.output?.UtxoTransactionOutput?.OutputIndex;
if (!txh || typeof idx !== 'number' || !isHex64(txh)) continue;
const key = `${txh}#${idx}`;
if (seen.has(key)) continue;
seen.add(key);
out.push(u);
}
// stable order
out.sort(
(a, b) =>
a.output.Transaction.Hash.localeCompare(b.output.Transaction.Hash) ||
a.output.UtxoTransactionOutput.OutputIndex - b.output.UtxoTransactionOutput.OutputIndex
);
return out;
};

// returns ALL UTXOs as TUSO CBOR hex
const getInputs = async (): Promise<string[]> => {
const utxos = sanitize(walletUtxos);
return utxos.map(encodeUtxo);
};

return { getInputs };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ export const useSyncedTokenInfos = ({
queryKey: ['syncedTokenInfos', networkId, primaryTokenInfo.id, ...excludedTokens],

queryFn: async () => {
// Hardcoded muesliswap because dexhunter is broken at the moment
await swapManager.assignSettings({
routingPreference: 'muesliswap',
});
const res = await swapManager.api.tokens();
if (!isRight(res)) return { tokenIds: [], tokenInfosArray: [] };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { tokenManagers } from '../../portfolio/common/helpers/build-token-manage
import { useSyncedTokenInfos } from '../common/hooks/useTokensInfo';
import { isLeft, isRight } from '@yoroi/common';
import { useGetInputs } from '../common/helpers';
import { ASSET_DIRECTION_IN } from '../common/constants';
import { ASSET_DIRECTION_IN, USDA_TOKEN_ID } from '../common/constants';

export const convertBech32ToHex = async (bech32Address: string) => {
return await RustModule.WalletV4.Address.from_bech32(bech32Address).to_hex();
Expand All @@ -49,7 +49,6 @@ export const SwapContextProvider = ({ children, currentWallet, stores }: any) =>
const tokenInInputRef = useRef<HTMLInputElement | null>(null);

const { getInputs } = useGetInputs(selectedWallet?.utxos || []);

const [state, action] = useReducer(swapReducer, defaultState);

useEffect(() => {
Expand Down Expand Up @@ -237,15 +236,8 @@ export const SwapContextProvider = ({ children, currentWallet, stores }: any) =>

const create = useCallback(async () => {
if (state.tokenInInput.tokenId === undefined || state.tokenOutInput.tokenId === undefined) return;

setIsCreateOrderLoading(true);

const tokenInInfo = tokenInfos.get(state.tokenInInput.tokenId);
const quantityIn =
Number(state.tokenInInput.value) *
10 ** (state.tokenInInput.tokenId === '.' ? primaryTokenInfo.decimals : tokenInInfo?.decimals);
const amountsIn = { [state.tokenInInput.tokenId]: String(quantityIn) };
const inputs = await getInputs(amountsIn);
const inputs = await getInputs();

swapManager.api
.create({
Expand Down Expand Up @@ -599,7 +591,7 @@ const defaultState: SwapState = Object.freeze({
},
tokenOutInput: {
isTouched: true,
tokenId: undefined,
tokenId: USDA_TOKEN_ID,
disabled: false,
error: null,
value: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,18 @@ const OrderCancelation = ({ order }: { order: Swap.Order }) => {
try {
startLoadingTxReview();
try {
await stores.transactionProcessingStore.adaSignTransactionHexFromWallet({
const { signedTxHex: signedCancelTx } = await stores.transactionProcessingStore.adaSignTransactionHexFromWallet({
wallet,
transactionHex: cancelTxCbor,
password: passswordInput,
});

const signedTransactionHexes: any = [signedCancelTx];
await stores.substores.ada.swapStore.executeTransactionHexes({
wallet,
signedTransactionHexes,
});

showTxResultModal(TransactionResult.SUCCESS);
} catch (error) {
console.warn('Failed to submit transaction', error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@ const getTokenName = (tokenInfo: any): string => {
export const TokenItem: React.FC<TokenItemProps> = ({ isSent = true, isPrimary, tokenInfo, quantity }: TokenItemProps) => {
const decimals = getDecimals(tokenInfo);
const tokenName = getTokenName(tokenInfo);

const value = new BigNumber(quantity).shiftedBy(-decimals).toString();
if (isSent) {
const primaryColor = isPrimary ? 'ds.white_static' : 'ds.text_primary_medium';
const primaryBackground = isPrimary ? 'ds.primary_500' : 'ds.primary_100';
return (
<Box sx={{ padding: '4px 12px', backgroundColor: primaryBackground, borderRadius: '8px', flexWrap: 'nowrap' }}>
<Box
sx={{
padding: '4px 12px',
backgroundColor: primaryBackground,
borderRadius: '8px',
flexWrap: 'nowrap',
}}
>
<Typography variant="body1" color={primaryColor}>
{value} {tokenName}
{value} {tokenName || tokenInfo?.ticker}
</Typography>
</Box>
);
Expand All @@ -58,7 +64,7 @@ export const TokenItem: React.FC<TokenItemProps> = ({ isSent = true, isPrimary,
return (
<Box sx={{ padding: '4px 12px', backgroundColor: primaryBackground, borderRadius: '8px', flexWrap: 'nowrap' }}>
<Typography variant="body1" color={primaryColor}>
{value} {tokenName}
{value} {tokenName || tokenInfo?.ticker}
</Typography>
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const useFormattedTx = (data: TransactionBody): FormattedTx => {

const formatInputs = (inputUtxos, allAssetList, networkId, primaryTokenInfo, walletAddresses): any => {
return inputUtxos.map(utxo => {
const address = utxo?.receiver;
const address = utxo?.receiver || utxo?.address;
const { resolvedAddress, paymentCredKind } = resolveAddress(address);

const rewardAddress = address !== null && paymentCredKind === CredKind.Key ? deriveAddress(address, networkId) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { Inputs } from '../UTxOs/UTxOsTab';
export const ReferenceInputsTab = ({ referenceInputs }) => {
return (
<Stack p="24px">
<Inputs key={`${referenceInputs.address}-${referenceInputs.txHash}-${referenceInputs.txIndex}`} inputs={referenceInputs} />
<Inputs
key={`${referenceInputs.address}-${referenceInputs.tx_hash}-${referenceInputs.tx_index}`}
inputs={referenceInputs}
/>
</Stack>
);
};
Loading