Skip to content

Commit a5ee3af

Browse files
fix: ton/starknet/aleo address validation (#1642)
Co-authored-by: Leo Sollier <[email protected]>
1 parent 610eb5a commit a5ee3af

File tree

10 files changed

+335
-48
lines changed

10 files changed

+335
-48
lines changed

packages/currency/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@
4545
"@metamask/contract-metadata": "1.31.0",
4646
"@requestnetwork/types": "0.54.0",
4747
"@requestnetwork/utils": "0.54.0",
48+
"@ton/core": "0.61.0",
49+
"@ton/crypto": "3.3.0",
50+
"bech32": "2.0.0",
4851
"multicoin-address-validator": "0.5.15",
4952
"node-dijkstra": "2.5.0",
53+
"starknet": "7.6.4",
5054
"tslib": "2.8.1"
5155
},
5256
"devDependencies": {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const chainId = 'aleo';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const chainId = 'starknet';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const chainId = 'ton';

packages/currency/src/chains/declarative/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ import { CurrencyTypes } from '@requestnetwork/types';
22

33
import * as TronDefinition from './data/tron';
44
import * as SolanaDefinition from './data/solana';
5+
import * as StarknetDefinition from './data/starknet';
6+
import * as TonDefinition from './data/ton';
7+
import * as AleoDefinition from './data/aleo';
58

69
export type DeclarativeChain = CurrencyTypes.Chain;
710

811
export const chains: Record<CurrencyTypes.DeclarativeChainName, DeclarativeChain> = {
912
tron: TronDefinition,
1013
solana: SolanaDefinition,
14+
starknet: StarknetDefinition,
15+
ton: TonDefinition,
16+
aleo: AleoDefinition,
1117
};

packages/currency/src/currencyManager.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { CurrencyTypes, RequestLogicTypes } from '@requestnetwork/types';
22
import { utils } from 'ethers';
3+
import { Address } from '@ton/core';
4+
import { validateAndParseAddress } from 'starknet';
35
import addressValidator from 'multicoin-address-validator';
6+
import { bech32 } from 'bech32';
47
import { getSupportedERC20Tokens } from './erc20';
58
import { getSupportedERC777Tokens } from './erc777';
69
import { getHash } from './getHash';
@@ -264,6 +267,12 @@ export class CurrencyManager<TMeta = unknown> implements CurrencyTypes.ICurrency
264267
return isValidNearAddress(address, currency.network);
265268
} else if (currency.network === 'tron' || currency.network === 'solana') {
266269
return addressValidator.validate(address, currency.network);
270+
} else if (currency.network === 'ton') {
271+
return this.validateTonAddress(address);
272+
} else if (currency.network === 'starknet') {
273+
return this.validateStarknetAddress(address);
274+
} else if (currency.network === 'aleo') {
275+
return this.validateAleoAddress(address);
267276
}
268277
return addressValidator.validate(address, 'ETH');
269278
case RequestLogicTypes.CURRENCY.BTC:
@@ -290,6 +299,54 @@ export class CurrencyManager<TMeta = unknown> implements CurrencyTypes.ICurrency
290299
return this.validateAddress(currency.value, currency);
291300
}
292301

302+
/**
303+
* Validate a TON address. See https://ton-org.github.io/ton-core/classes/Address.html#parse for more details.
304+
* @param address - The address to validate
305+
* @returns True if the address is valid, false otherwise
306+
*/
307+
validateTonAddress(address: string): boolean {
308+
try {
309+
return !!Address.parse(address);
310+
} catch {
311+
return false;
312+
}
313+
}
314+
315+
/**
316+
* Validate a Starknet address. See https://starknetjs.com/docs/next/API/modules/#validateandparseaddress for more details.
317+
* @param address - The address to validate
318+
* @returns True if the address is valid, false otherwise
319+
*/
320+
validateStarknetAddress(address: string): boolean {
321+
try {
322+
return !!validateAndParseAddress(address);
323+
} catch {
324+
return false;
325+
}
326+
}
327+
328+
/**
329+
* Validate an Aleo address using proper Bech32 validation with checksum verification.
330+
* Aleo addresses use Bech32 encoding with:
331+
* - HRP (Human Readable Part): "aleo"
332+
* - Separator: "1"
333+
* - Data + checksum: 58 characters
334+
* - Total length: 63 characters
335+
* - Strict Bech32 character set with checksum validation
336+
*
337+
* See https://namespaces.chainagnostic.org/aleo/caip10 for more details.
338+
* @param address - The address to validate
339+
* @returns True if the address is valid, false otherwise
340+
*/
341+
validateAleoAddress(address: string): boolean {
342+
try {
343+
const { prefix } = bech32.decode(address);
344+
return prefix === 'aleo';
345+
} catch {
346+
return false;
347+
}
348+
}
349+
293350
/**
294351
* Returns the list of currencies supported by Request out of the box
295352
* Contains:

packages/currency/test/currencyManager.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,4 +765,46 @@ describe('CurrencyManager', () => {
765765
});
766766
});
767767
});
768+
769+
describe('validateAleoAddress', () => {
770+
it('should validate correct Aleo addresses', () => {
771+
const validAddress = 'aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8sta57j8';
772+
expect(currencyManager.validateAleoAddress(validAddress)).toBe(true);
773+
expect(currencyManager.validateAleoAddress(validAddress.toUpperCase())).toBe(true);
774+
});
775+
776+
it('should reject invalid Aleo addresses', () => {
777+
const invalidAddresses = [
778+
// Empty or null inputs
779+
'',
780+
' ',
781+
null,
782+
undefined,
783+
// Wrong prefix
784+
'bitcoin1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8sta57j8',
785+
'cosmos1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8sta57j8',
786+
// Mixed case
787+
'aleo1Qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8sta57j8',
788+
// Wrong format
789+
'aleo1',
790+
'aleo1abc',
791+
'not-an-address',
792+
'random-string',
793+
// Invalid characters that would pass simple regex but fail Bech32
794+
'aleo1' + 'b'.repeat(58), // 'b' not in Bech32 alphabet
795+
'aleo1' + 'i'.repeat(58), // 'i' not in Bech32 alphabet
796+
'aleo1' + 'o'.repeat(58), // 'o' not in Bech32 alphabet
797+
// Non-string inputs
798+
123,
799+
{},
800+
[],
801+
// valid address with whitespace
802+
' aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8sta57j8 ',
803+
];
804+
805+
invalidAddresses.forEach((address) => {
806+
expect(currencyManager.validateAleoAddress(address as any)).toBe(false);
807+
});
808+
});
809+
});
768810
});

packages/request-client.js/test/index.test.ts

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ExtensionTypes,
66
IdentityTypes,
77
PaymentTypes,
8+
CurrencyTypes,
89
RequestLogicTypes,
910
} from '@requestnetwork/types';
1011
import { decrypt, random32Bytes } from '@requestnetwork/utils';
@@ -1647,48 +1648,59 @@ describe('request-client.js', () => {
16471648
expect(data.expectedAmount).toBe(requestParameters.expectedAmount);
16481649
});
16491650

1650-
it('Can create ERC20 declarative requests with non-evm currency - solana', async () => {
1651-
const testErc20TokenAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
1652-
const requestNetwork = new RequestNetwork({
1653-
signatureProvider: TestData.fakeSignatureProvider,
1654-
useMockStorage: true,
1655-
});
1651+
describe('Can create ERC20 declarative requests with non-evm currencies', () => {
1652+
const cases: Array<[CurrencyTypes.DeclarativeChainName, string]> = [
1653+
['solana', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
1654+
['ton', 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs'],
1655+
['starknet', '0x028757d11c97078Dd182023B1cC7b9E7659716c631ADF94D24f1fa7Dc5943072'],
1656+
['aleo', 'aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8sta57j8'],
1657+
];
1658+
1659+
it.each(cases)(
1660+
'Can create ERC20 declarative requests with non-evm currency - %s',
1661+
async (network, tokenAddress) => {
1662+
const requestNetwork = new RequestNetwork({
1663+
signatureProvider: TestData.fakeSignatureProvider,
1664+
useMockStorage: true,
1665+
});
16561666

1657-
const paymentNetwork: PaymentTypes.PaymentNetworkCreateParameters = {
1658-
id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE,
1659-
parameters: {},
1660-
};
1667+
const paymentNetwork: PaymentTypes.PaymentNetworkCreateParameters = {
1668+
id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE,
1669+
parameters: {},
1670+
};
16611671

1662-
const requestInfo = Object.assign({}, TestData.parametersWithoutExtensionsData, {
1663-
currency: {
1664-
network: 'solana',
1665-
type: RequestLogicTypes.CURRENCY.ERC20,
1666-
value: testErc20TokenAddress,
1667-
},
1668-
});
1672+
const requestInfo = Object.assign({}, TestData.parametersWithoutExtensionsData, {
1673+
currency: {
1674+
network,
1675+
type: RequestLogicTypes.CURRENCY.ERC20,
1676+
value: tokenAddress,
1677+
},
1678+
});
16691679

1670-
const request = await requestNetwork.createRequest({
1671-
paymentNetwork,
1672-
requestInfo,
1673-
signer: TestData.payee.identity,
1674-
});
1680+
const request = await requestNetwork.createRequest({
1681+
paymentNetwork,
1682+
requestInfo,
1683+
signer: TestData.payee.identity,
1684+
});
16751685

1676-
await new Promise((resolve): any => setTimeout(resolve, 150));
1677-
let data = await request.refresh();
1686+
await new Promise((resolve): any => setTimeout(resolve, 150));
1687+
const data = await request.refresh();
16781688

1679-
expect(data).toBeDefined();
1680-
expect(data.balance?.balance).toBe('0');
1681-
expect(data.balance?.events.length).toBe(0);
1682-
expect(data.meta).toBeDefined();
1683-
expect(data.currency).toBe('unknown');
1689+
expect(data).toBeDefined();
1690+
expect(data.balance?.balance).toBe('0');
1691+
expect(data.balance?.events.length).toBe(0);
1692+
expect(data.meta).toBeDefined();
1693+
expect(data.currency).toBe('unknown');
16841694

1685-
expect(data.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE].values).toEqual({
1686-
receivedPaymentAmount: '0',
1687-
receivedRefundAmount: '0',
1688-
sentPaymentAmount: '0',
1689-
sentRefundAmount: '0',
1690-
});
1691-
expect(data.expectedAmount).toBe(requestParameters.expectedAmount);
1695+
expect(data.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE].values).toEqual({
1696+
receivedPaymentAmount: '0',
1697+
receivedRefundAmount: '0',
1698+
sentPaymentAmount: '0',
1699+
sentRefundAmount: '0',
1700+
});
1701+
expect(data.expectedAmount).toBe(requestParameters.expectedAmount);
1702+
},
1703+
);
16921704
});
16931705

16941706
it('cannot create ERC20 address based requests with invalid currency', async () => {

packages/types/src/currency-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export type BtcChainName = 'mainnet' | 'testnet';
4242
/**
4343
* List of supported Declarative chains
4444
*/
45-
export type DeclarativeChainName = 'tron' | 'solana';
45+
export type DeclarativeChainName = 'tron' | 'solana' | 'ton' | 'starknet' | 'aleo';
4646

4747
/**
4848
* List of supported NEAR chains

0 commit comments

Comments
 (0)