Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
110 changes: 110 additions & 0 deletions __tests__/hooks/useTokenLookup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,46 @@ jest.mock("services/stellarExpert", () => ({
},
});
}
if (searchTerm === "dejaaa") {
return Promise.resolve({
_embedded: {
records: [
{
asset: "CC64WBDGS6QQP22QTTIACYIXT3WF7BBQEYOQPLTP7GTKYY7PZ74QYGSL",
domain: "centrifuge.io",
code: "deJAAA",
token_name: "deJAAA",
decimals: 18,
tomlInfo: {
code: "deJAAA",
issuer:
"CBSZNRNQIFHKHBHPZEHJFWGTJMPLYN4NJYGTLEJBHUVQTTOQ2WSQ6OMV",
image:
"https://stellar.myfilebase.com/ipfs/QmXu7RUtm2zhGDhk1ScDSWH587xccMm8ewprrFr4rYX6F3",
name: "deJAAA",
decimals: 18,
},
},
{
asset:
"DEJAAA-GASIX3XBHMFJGHOMC2FEELMO2E6JYE5LL2V2QBW3GX23GJI2EHWM4R5E-2",
},
],
},
});
}
if (searchTerm === "contract-only") {
return Promise.resolve({
_embedded: {
records: [
{
asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
domain: "example.com",
},
],
},
});
}
if (searchTerm === "error") {
return Promise.reject(new Error("Search failed"));
}
Expand Down Expand Up @@ -294,6 +334,76 @@ describe("useTokenLookup", () => {
expect(tokens).not.toContain("FOO");
});

it("should parse contract tokens from stellar.expert search results", async () => {
const { result } = renderHook(() =>
useTokenLookup({
network: mockNetwork,
publicKey: mockPublicKey,
balanceItems: mockBalanceItems,
}),
);

await act(async () => {
result.current.handleSearch("dejaaa");
await flushPromises();
});

expect(result.current.status).toBe(HookStatus.SUCCESS);
expect(result.current.searchResults).toHaveLength(2);

// First result: contract token — should use top-level `code`, not the raw contract ID
const contractToken = result.current.searchResults[0];
expect(contractToken.tokenCode).toBe("deJAAA");
expect(contractToken.issuer).toBe(
"CC64WBDGS6QQP22QTTIACYIXT3WF7BBQEYOQPLTP7GTKYY7PZ74QYGSL",
);
expect(contractToken.domain).toBe("centrifuge.io");
expect(contractToken.tokenType).toBe(TokenTypeWithCustomToken.CUSTOM_TOKEN);
expect(contractToken.decimals).toBe(18);
expect(contractToken.name).toBe("deJAAA");
expect(contractToken.iconUrl).toBe(
"https://stellar.myfilebase.com/ipfs/QmXu7RUtm2zhGDhk1ScDSWH587xccMm8ewprrFr4rYX6F3",
);

// Second result: classic asset — should be parsed with split("-")
const classicToken = result.current.searchResults[1];
expect(classicToken.tokenCode).toBe("DEJAAA");
expect(classicToken.issuer).toBe(
"GASIX3XBHMFJGHOMC2FEELMO2E6JYE5LL2V2QBW3GX23GJI2EHWM4R5E",
);
});

it("should fall back to contract ID as tokenCode when metadata is missing", async () => {
const { result } = renderHook(() =>
useTokenLookup({
network: mockNetwork,
publicKey: mockPublicKey,
balanceItems: mockBalanceItems,
}),
);

await act(async () => {
result.current.handleSearch("contract-only");
await flushPromises();
});

expect(result.current.status).toBe(HookStatus.SUCCESS);
expect(result.current.searchResults).toHaveLength(1);

const token = result.current.searchResults[0];
// No `code` or `tomlInfo.code` — falls back to contract ID
expect(token.tokenCode).toBe(
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
);
expect(token.issuer).toBe(
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
);
expect(token.domain).toBe("example.com");
expect(token.tokenType).toBe(TokenTypeWithCustomToken.CUSTOM_TOKEN);
expect(token.decimals).toBeUndefined();
expect(token.iconUrl).toBeUndefined();
});

it("should handle contract ID search", async () => {
const { result } = renderHook(() =>
useTokenLookup({
Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1234567890
versionName "1.13.25"
versionName "1.14.25"
}

buildTypes {
Expand Down
8 changes: 4 additions & 4 deletions ios/freighter-mobile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.13.25;
MARKETING_VERSION = 1.14.25;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down Expand Up @@ -542,7 +542,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.13.25;
MARKETING_VERSION = 1.14.25;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down Expand Up @@ -740,7 +740,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.13.25;
MARKETING_VERSION = 1.14.25;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down Expand Up @@ -775,7 +775,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.13.25;
MARKETING_VERSION = 1.14.25;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down
2 changes: 1 addition & 1 deletion ios/freighter-mobile/Info-Dev.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.13.25</string>
<string>1.14.25</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand Down
2 changes: 1 addition & 1 deletion ios/freighter-mobile/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.13.25</string>
<string>1.14.25</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "freighter-mobile",
"version": "1.13.25",
"version": "1.14.25",
"license": "Apache-2.0",
"scripts": {
"android": "yarn android-dev",
Expand Down
28 changes: 20 additions & 8 deletions scripts/setup-xcode-latest-stable
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

set -e

# Maximum allowed Xcode major.minor version.
# Bump this once all Pods (especially `fmt`) compile cleanly on the next Xcode.
MAX_XCODE_VERSION="26.3"

# Find all beta/RC Xcode versions to identify base versions to exclude
BETA_XCODE_PATHS=$(ls -1d /Applications/Xcode*.app 2>/dev/null | grep -E "(Release_Candidate|RC|beta|Beta)" || true)

Expand Down Expand Up @@ -44,15 +48,23 @@ for XCODE_PATH in $XCODE_PATHS; do
# Extract major.minor version for comparison
BASE_VERSION=$(echo "$VERSION" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/')

# Check if this base version should be excluded
# Check if this version exceeds the maximum allowed version
SHOULD_EXCLUDE=false
for EXCLUDED_BASE in $EXCLUDED_BASE_VERSIONS; do
if [ "$BASE_VERSION" = "$EXCLUDED_BASE" ]; then
echo "Excluding $XCODE_PATH (Version: $VERSION, Base: $BASE_VERSION matches excluded base version)"
SHOULD_EXCLUDE=true
break
fi
done
if [ -n "$MAX_XCODE_VERSION" ] && [ "$(printf '%s\n%s' "$MAX_XCODE_VERSION" "$BASE_VERSION" | sort -V | head -n1)" != "$BASE_VERSION" ]; then
echo "Excluding $XCODE_PATH (Version: $VERSION exceeds max allowed $MAX_XCODE_VERSION)"
SHOULD_EXCLUDE=true
fi

# Check if this base version should be excluded (beta/RC)
if [ "$SHOULD_EXCLUDE" = "false" ]; then
for EXCLUDED_BASE in $EXCLUDED_BASE_VERSIONS; do
if [ "$BASE_VERSION" = "$EXCLUDED_BASE" ]; then
echo "Excluding $XCODE_PATH (Version: $VERSION, Base: $BASE_VERSION matches excluded base version)"
SHOULD_EXCLUDE=true
break
fi
done
fi

if [ "$SHOULD_EXCLUDE" = "false" ]; then
printf "%s\t%s\n" "$VERSION" "$XCODE_PATH" >> "$TEMP_FILE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,23 @@ const AddTokenBottomSheetContent: React.FC<AddTokenBottomSheetContentProps> = ({
const listItems = useMemo(() => {
if (!token) return [];

// If issuer is already a contract ID, use it directly
// Otherwise, create an Asset and get its contract ID
const tokenContractId = isContractId(token.issuer)
? token.issuer
: new Asset(token.tokenCode, token.issuer).contractId(networkPassphrase);
// For contract tokens (Soroban), the contract ID is typically stored in
// `issuer`. As a safety fallback, stellar.expert can return contract
// tokens where `asset` is the raw contract ID, so `tokenCode` may hold
// the contract ID instead. Select the contract-bearing field explicitly:
// a non-empty classic issuer must not override a contract ID found in
// `tokenCode`.
const getTokenContractId = () => {
if (isContractId(token.issuer)) return token.issuer;
if (isContractId(token.tokenCode)) return token.tokenCode;

// without the above guard, `new Asset()` would
// throw because contract IDs exceed the 12-char asset code limit
return new Asset(token.tokenCode, token.issuer).contractId(
networkPassphrase,
);
};
const tokenContractId = getTokenContractId();

const items = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ const SwapAmountScreen: React.FC<SwapAmountScreenProps> = ({
duration: 0,
});
}
}, [pathError, sourceAmount, destinationTokenId, activeError]);
}, [pathError, sourceAmount, destinationTokenId]);

const handleSettingsPress = useCallback(() => {
transactionSettingsBottomSheetModalRef.current?.present();
Expand Down
9 changes: 7 additions & 2 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,16 @@ export interface SearchTokenResponse {
};
score: number;
paging_token: number;
tomlInfo: {
tomlInfo?: {
code: string;
image: string;
image?: string;
issuer: string;
decimals?: number;
name?: string;
};
code?: string;
token_name?: string;
decimals?: number;
}[];
};
}
Expand Down
18 changes: 18 additions & 0 deletions src/ducks/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { logger } from "config/logger";
import { PricedBalance } from "config/types";
import { useDebugStore } from "ducks/debug";
import { formatBigNumberForDisplay } from "helpers/formatAmount";
import { isContractId } from "helpers/soroban";
import { t } from "i18next";
import { getTokenForPayment } from "services/transactionService";
import { create } from "zustand";
Expand Down Expand Up @@ -184,6 +185,23 @@ export const useSwapStore = create<SwapState>((set) => ({
throw new Error(t("debug.debugMessages.swapPathFailure"));
}

// Custom/Soroban tokens cannot be swapped via Horizon's classic DEX
const isSourceCustom =
"contractId" in sourceBalance ||
("id" in sourceBalance && isContractId(sourceBalance.id ?? ""));
const isDestCustom =
"contractId" in destinationBalance ||
("id" in destinationBalance &&
isContractId(destinationBalance.id ?? ""));

if (isSourceCustom || isDestCustom) {
set({
isLoadingPath: false,
pathError: t("swapScreen.errors.customTokenSwapNotSupported"),
});
return;
}

// For now, we only support classic path payments
// TODO: Add Soroswap support for Testnet in future iteration
const pathResult = await findClassicSwapPath({
Expand Down
Loading
Loading