Skip to content

Commit b7f6da6

Browse files
Fix/stellar OK-48532 OK-48533 OK-48666 (#9600)
* fix: stellar memo * fix: stellar memo
1 parent dd4ac76 commit b7f6da6

File tree

34 files changed

+269
-88
lines changed

34 files changed

+269
-88
lines changed

packages/kit-bg/src/services/ServiceCustomToken.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,6 @@ class ServiceCustomToken extends ServiceBase {
314314
}
315315

316316
@backgroundMethod()
317-
@toastIfError()
318317
async activateToken({
319318
accountId,
320319
networkId,

packages/kit-bg/src/services/ServiceSend.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,23 @@ class ServiceSend extends ServiceBase {
939939
);
940940
return resp.data.data;
941941
}
942+
943+
@backgroundMethod()
944+
async validateMemo(params: {
945+
networkId: string;
946+
accountId?: string;
947+
memo: string;
948+
}) {
949+
const { networkId, accountId, memo } = params;
950+
if (accountId) {
951+
const vault = await vaultFactory.getVault({ networkId, accountId });
952+
return vault.validateMemo(memo);
953+
}
954+
955+
return (await vaultFactory.getChainOnlyVault({ networkId })).validateMemo(
956+
memo,
957+
);
958+
}
942959
}
943960

944961
export default ServiceSend;

packages/kit-bg/src/vaults/base/VaultBase.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ export abstract class VaultBaseChainOnly extends VaultContext {
192192
params: IValidateGeneralInputParams,
193193
): Promise<IGeneralInputValidation>;
194194

195+
/**
196+
* Validate memo/tag field (optional, chain-specific implementation)
197+
* @param memo - The memo string to validate
198+
* @returns Validation result with error message if invalid
199+
*/
200+
async validateMemo(memo: string): Promise<{
201+
isValid: boolean;
202+
errorMessage?: string;
203+
}> {
204+
// Default implementation: always valid (chains can override)
205+
return { isValid: true };
206+
}
207+
195208
async baseValidatePrivateKey(
196209
privateKey: string,
197210
): Promise<IPrivateKeyValidation> {

packages/kit-bg/src/vaults/impls/stellar/Vault.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
OneKeyInternalError,
2424
} from '@onekeyhq/shared/src/errors';
2525
import { ETranslations } from '@onekeyhq/shared/src/locale';
26+
import { appLocale } from '@onekeyhq/shared/src/locale/appLocale';
2627
import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils';
2728
import { memoizee } from '@onekeyhq/shared/src/utils/cacheUtils';
2829
import timerUtils from '@onekeyhq/shared/src/utils/timerUtils';
@@ -89,12 +90,14 @@ import { EStellarAssetType } from './types';
8990
import {
9091
BASE_FEE,
9192
ENTRY_RESERVE,
93+
MEMO_TEXT_MAX_BYTES,
9294
SAC_TOKEN_ASSET_TYPES,
9395
SAC_TOKEN_DECIMALS,
9496
buildMemoFromString,
9597
calculateAvailableBalance,
9698
calculateFrozenBalance,
9799
getNetworkPassphrase,
100+
getUtf8ByteLength,
98101
isValidAccountCreationAmount,
99102
parseTokenAddress,
100103
} from './utils';
@@ -1074,6 +1077,35 @@ export default class Vault extends VaultBase {
10741077
return result;
10751078
}
10761079

1080+
override async validateMemo(memo: string): Promise<{
1081+
isValid: boolean;
1082+
errorMessage?: string;
1083+
}> {
1084+
if (!memo || !memo.trim()) {
1085+
return { isValid: true }; // Empty memo is valid
1086+
}
1087+
1088+
const trimmed = memo.trim();
1089+
1090+
// Text memo: check byte length
1091+
const byteLength = getUtf8ByteLength(trimmed);
1092+
if (byteLength > MEMO_TEXT_MAX_BYTES) {
1093+
return {
1094+
isValid: false,
1095+
errorMessage: appLocale.intl.formatMessage(
1096+
{ id: ETranslations.send_memo_size_exceeded },
1097+
{
1098+
limit: MEMO_TEXT_MAX_BYTES,
1099+
current: byteLength,
1100+
type: 'Bytes',
1101+
},
1102+
),
1103+
};
1104+
}
1105+
1106+
return { isValid: true };
1107+
}
1108+
10771109
// ========== LOCAL DEVELOPMENT RPC SUPPORT ==========
10781110
private _getCustomClientCache = memoizee(
10791111
async (url: string): Promise<ClientStellar> => {

packages/kit-bg/src/vaults/impls/stellar/sdkStellar/ClientStellar.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ export default class ClientStellar {
6868
*/
6969
async accountExists(address: string): Promise<boolean> {
7070
try {
71-
await this.getAccountInfo(address);
71+
const accountInfo = await this.getAccountInfo(address);
72+
if (!accountInfo) {
73+
return false;
74+
}
7275
return true;
7376
} catch (error) {
7477
return false;

packages/kit-bg/src/vaults/impls/stellar/settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const settings: IVaultSettings = {
4545
hasFrozenBalance: true, // trustline count * 0.5XLM is frozen balance
4646

4747
withMemo: true,
48-
memoMaxLength: 28,
48+
supportMemoValidation: true, // Use Vault.validateMemo() for precise validation
4949

5050
accountDeriveInfo,
5151
customRpcEnabled: true,

packages/kit-bg/src/vaults/impls/stellar/utils.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,19 @@ export const SAC_TOKEN_DECIMALS = 7;
2121

2222
export const SAC_TOKEN_ASSET_TYPES = ['credit_alphanum4', 'credit_alphanum12'];
2323

24-
const MEMO_TEXT_MAX_BYTES = 28;
24+
export const MEMO_TEXT_MAX_BYTES = 28;
2525

2626
const MEMO_ID_MAX = new BigNumber('18446744073709551615');
2727

28+
/**
29+
* Calculate the byte length of a UTF-8 string
30+
* @param text - The text to measure
31+
* @returns The byte length
32+
*/
33+
export function getUtf8ByteLength(text: string): number {
34+
return Buffer.from(text, 'utf8').length;
35+
}
36+
2837
export function getNetworkPassphrase(networkId: string): string {
2938
return networkId.includes('testnet') ? Networks.TESTNET : Networks.PUBLIC;
3039
}
@@ -74,9 +83,11 @@ export function buildMemoFromString(memo?: string) {
7483
if (isUint64Memo(trimmed)) {
7584
return Memo.id(trimmed);
7685
}
77-
const memoBytes = Buffer.from(trimmed, 'utf8');
78-
if (memoBytes.length > MEMO_TEXT_MAX_BYTES) {
79-
throw new OneKeyInternalError('Memo text exceeds 28 bytes limit');
86+
const byteLength = getUtf8ByteLength(trimmed);
87+
if (byteLength > MEMO_TEXT_MAX_BYTES) {
88+
throw new OneKeyInternalError(
89+
`Memo text exceeds ${MEMO_TEXT_MAX_BYTES} bytes limit (current: ${byteLength} bytes)`,
90+
);
8091
}
8192
return Memo.text(trimmed);
8293
}

packages/kit-bg/src/vaults/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,13 @@ export type IVaultSettings = {
212212
* https://support.ledger.com/hc/en-us/articles/4409603715217-What-is-a-Memo-Tag-?support=true
213213
*/
214214
withMemo?: boolean;
215-
memoMaxLength?: number;
215+
memoMaxLength?: number; // Fallback: character-based limit (legacy)
216216
numericOnlyMemo?: boolean;
217+
/**
218+
* If true, Vault has implemented validateMemo() for precise validation
219+
* Form validation will call vault.validateMemo() instead of using memoMaxLength
220+
*/
221+
supportMemoValidation?: boolean;
217222

218223
// dnx
219224
withPaymentId?: boolean;

packages/kit/src/views/AddressBook/components/CreateOrEditContent.tsx

Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -186,57 +186,86 @@ export function CreateOrEditContent({
186186
);
187187
}, [intl, media.gtMd, vaultSettings?.noteMaxLength, vaultSettings?.withNote]);
188188

189+
const validateMemoField = useCallback(
190+
async (value: string): Promise<string | undefined> => {
191+
if (!value) return undefined;
192+
193+
try {
194+
const validationResult =
195+
await backgroundApiProxy.serviceSend.validateMemo({
196+
networkId,
197+
memo: value,
198+
});
199+
if (!validationResult.isValid) {
200+
return validationResult.errorMessage;
201+
}
202+
return undefined;
203+
} catch (error) {
204+
// Fallback to client-side validation if Vault validation fails
205+
console.warn('Vault validateMemo failed, using fallback:', error);
206+
}
207+
208+
// Fallback: use original logic
209+
const validateErrMsg = vaultSettings?.numericOnlyMemo
210+
? intl.formatMessage({
211+
id: ETranslations.send_field_only_integer,
212+
})
213+
: undefined;
214+
const memoRegExp = vaultSettings?.numericOnlyMemo
215+
? /^[0-9]+$/
216+
: undefined;
217+
218+
if (!value || !memoRegExp) return undefined;
219+
const result = !memoRegExp.test(value);
220+
return result ? validateErrMsg : undefined;
221+
},
222+
[intl, networkId, vaultSettings?.numericOnlyMemo],
223+
);
224+
189225
const renderMemoForm = useCallback(() => {
190226
if (!vaultSettings?.withMemo) return null;
227+
191228
const maxLength = vaultSettings?.memoMaxLength || 256;
192-
const validateErrMsg = vaultSettings?.numericOnlyMemo
193-
? intl.formatMessage({
194-
id: ETranslations.send_field_only_integer,
195-
})
196-
: undefined;
197-
const memoRegExp = vaultSettings?.numericOnlyMemo ? /^[0-9]+$/ : undefined;
229+
const customValidate = vaultSettings?.supportMemoValidation;
198230

199231
return (
200-
<>
201-
<Form.Field
202-
label={intl.formatMessage({ id: ETranslations.send_tag })}
203-
optional
204-
name="memo"
205-
rules={{
206-
maxLength: {
207-
value: maxLength,
208-
message: intl.formatMessage(
209-
{
210-
id: ETranslations.dapp_connect_msg_description_can_be_up_to_int_characters,
211-
},
212-
{
213-
number: maxLength,
214-
},
215-
),
216-
},
217-
validate: (value) => {
218-
if (!value || !memoRegExp) return undefined;
219-
const result = !memoRegExp.test(value);
220-
return result ? validateErrMsg : undefined;
221-
},
222-
}}
223-
>
224-
<TextAreaInput
225-
numberOfLines={2}
226-
size={media.gtMd ? 'medium' : 'large'}
227-
placeholder={intl.formatMessage({
228-
id: ETranslations.send_tag_placeholder,
229-
})}
230-
/>
231-
</Form.Field>
232-
</>
232+
<Form.Field
233+
label={intl.formatMessage({ id: ETranslations.send_tag })}
234+
optional
235+
name="memo"
236+
rules={{
237+
maxLength: customValidate
238+
? undefined
239+
: {
240+
value: maxLength,
241+
message: intl.formatMessage(
242+
{
243+
id: ETranslations.dapp_connect_msg_description_can_be_up_to_int_characters,
244+
},
245+
{
246+
number: maxLength,
247+
},
248+
),
249+
},
250+
validate: validateMemoField,
251+
}}
252+
>
253+
<TextAreaInput
254+
numberOfLines={2}
255+
size={media.gtMd ? 'medium' : 'large'}
256+
placeholder={intl.formatMessage({
257+
id: ETranslations.send_tag_placeholder,
258+
})}
259+
/>
260+
</Form.Field>
233261
);
234262
}, [
235263
intl,
236264
media.gtMd,
265+
validateMemoField,
237266
vaultSettings?.memoMaxLength,
238-
vaultSettings?.numericOnlyMemo,
239267
vaultSettings?.withMemo,
268+
vaultSettings?.supportMemoValidation,
240269
]);
241270

242271
return (

packages/kit/src/views/Borrow/pages/ReserveDetails/components/InterestRateModelChart.native.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,9 @@ export function InterestRateModelChart({
359359
}, [chartConfig, webViewReady]);
360360

361361
const utilizationPercentage = utilizationRatio
362-
? `${(normalizeUtilization(parseFloat(utilizationRatio)) * 100).toFixed(2)}%`
362+
? `${(normalizeUtilization(parseFloat(utilizationRatio)) * 100).toFixed(
363+
2,
364+
)}%`
363365
: '0.00%';
364366

365367
if (isLoading) {

0 commit comments

Comments
 (0)