Skip to content

Commit bc3f700

Browse files
feat/starknet-utils
1 parent 0eb975f commit bc3f700

6 files changed

Lines changed: 87 additions & 1 deletion

File tree

.changeset/metal-coats-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/utils': minor
3+
---
4+
5+
Add Starknet address and tx utils

typescript/utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"ethers": "^5.7.2",
1010
"lodash-es": "^4.17.21",
1111
"pino": "^8.19.0",
12+
"starknet": "^6.24.1",
1213
"yaml": "2.4.5"
1314
},
1415
"devDependencies": {

typescript/utils/src/addresses.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ const COS_NON_ZERO_ADDR =
1515
'neutron1jyyjd3x0jhgswgm6nnctxvzla8ypx50tew3ayxxwkrjfxhvje6kqzvzudq';
1616
const SOL_ZERO_ADDR = '111111';
1717
const SOL_NON_ZERO_ADDR = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
18+
const STARKNET_ZERO_ADDR =
19+
'0x0000000000000000000000000000000000000000000000000000000000000000';
20+
const STARKNET_NON_ZERO_ADDR =
21+
'0x0000000000000000000000000000000000000000000000000000000000000001';
1822

1923
// TODO increase address utility test coverage
2024
describe('Address utilities', () => {
@@ -24,22 +28,26 @@ describe('Address utilities', () => {
2428
expect(isZeroishAddress(ETH_ZERO_ADDR)).to.be.true;
2529
expect(isZeroishAddress(COS_ZERO_ADDR)).to.be.true;
2630
expect(isZeroishAddress(SOL_ZERO_ADDR)).to.be.true;
31+
expect(isZeroishAddress(STARKNET_ZERO_ADDR)).to.be.true;
2732
});
2833
it('Identifies non-0-ish addresses', () => {
2934
expect(isZeroishAddress(ETH_NON_ZERO_ADDR)).to.be.false;
3035
expect(isZeroishAddress(COS_NON_ZERO_ADDR)).to.be.false;
3136
expect(isZeroishAddress(SOL_NON_ZERO_ADDR)).to.be.false;
37+
expect(isZeroishAddress(STARKNET_NON_ZERO_ADDR)).to.be.false;
3238
});
3339
});
3440

3541
describe('addressToBytes', () => {
3642
it('Converts addresses to bytes', () => {
3743
expect(addressToBytes(ETH_NON_ZERO_ADDR).length).to.equal(32);
44+
expect(addressToBytes(STARKNET_NON_ZERO_ADDR).length).to.equal(32);
3845
});
3946
it('Rejects zeroish addresses', () => {
4047
expect(() => addressToBytes(ETH_ZERO_ADDR)).to.throw(Error);
4148
expect(() => addressToBytes(COS_ZERO_ADDR)).to.throw(Error);
4249
expect(() => addressToBytes(SOL_ZERO_ADDR)).to.throw(Error);
50+
expect(() => addressToBytes(STARKNET_ZERO_ADDR)).to.throw(Error);
4351
});
4452
});
4553

@@ -62,6 +70,12 @@ describe('Address utilities', () => {
6270
ProtocolType.Ethereum,
6371
),
6472
).to.equal(ETH_NON_ZERO_ADDR);
73+
expect(
74+
bytesToProtocolAddress(
75+
addressToBytes(STARKNET_NON_ZERO_ADDR),
76+
ProtocolType.Starknet,
77+
),
78+
).to.equal(STARKNET_NON_ZERO_ADDR);
6579
});
6680
it('Rejects zeroish addresses', () => {
6781
expect(() =>

typescript/utils/src/addresses.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding';
22
import { PublicKey } from '@solana/web3.js';
33
import { Wallet, utils as ethersUtils } from 'ethers';
4+
import {
5+
addAddressPadding,
6+
encode,
7+
num,
8+
validateAndParseAddress,
9+
} from 'starknet';
410

511
import { isNullish } from './typeof.js';
612
import { Address, HexString, ProtocolType } from './types.js';
713
import { assert } from './validation.js';
814

915
const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
1016
const SEALEVEL_ADDRESS_REGEX = /^[a-zA-Z0-9]{36,44}$/;
17+
const STARKNET_ADDRESS_REGEX = /^(0x)?[0-9a-fA-F]{64}$/;
1118

1219
const HEX_BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/;
1320

@@ -24,10 +31,12 @@ const COSMOS_FACTORY_TOKEN_REGEX = new RegExp(
2431
const EVM_TX_HASH_REGEX = /^0x([A-Fa-f0-9]{64})$/;
2532
const SEALEVEL_TX_HASH_REGEX = /^[a-zA-Z1-9]{88}$/;
2633
const COSMOS_TX_HASH_REGEX = /^(0x)?[A-Fa-f0-9]{64}$/;
34+
const STARKNET_TX_HASH_REGEX = /^(0x)?[0-9a-fA-F]{64}$/;
2735

2836
const EVM_ZEROISH_ADDRESS_REGEX = /^(0x)?0*$/;
2937
const SEALEVEL_ZEROISH_ADDRESS_REGEX = /^1+$/;
3038
const COSMOS_ZEROISH_ADDRESS_REGEX = /^[a-z]{1,10}?1[0]+$/;
39+
const STARKNET_ZEROISH_ADDRESS_REGEX = /^(0x)?0*$/;
3140

3241
export const ZERO_ADDRESS_HEX_32 =
3342
'0x0000000000000000000000000000000000000000000000000000000000000000';
@@ -48,6 +57,10 @@ export function isAddressCosmos(address: Address) {
4857
);
4958
}
5059

60+
export function isAddressStarknet(address: Address) {
61+
return STARKNET_ADDRESS_REGEX.test(address);
62+
}
63+
5164
export function getAddressProtocolType(address: Address) {
5265
if (!address) return undefined;
5366
if (isAddressEvm(address)) {
@@ -56,6 +69,8 @@ export function getAddressProtocolType(address: Address) {
5669
return ProtocolType.Cosmos;
5770
} else if (isAddressSealevel(address)) {
5871
return ProtocolType.Sealevel;
72+
} else if (isAddressStarknet(address)) {
73+
return ProtocolType.Starknet;
5974
} else {
6075
return undefined;
6176
}
@@ -112,13 +127,23 @@ export function isValidAddressCosmos(address: Address) {
112127
}
113128
}
114129

130+
export function isValidAddressStarknet(address: Address) {
131+
try {
132+
const isValid = address && validateAndParseAddress(address);
133+
return !!isValid;
134+
} catch {
135+
return false;
136+
}
137+
}
138+
115139
export function isValidAddress(address: Address, protocol?: ProtocolType) {
116140
return routeAddressUtil(
117141
{
118142
[ProtocolType.Ethereum]: isValidAddressEvm,
119143
[ProtocolType.Sealevel]: isValidAddressSealevel,
120144
[ProtocolType.Cosmos]: isValidAddressCosmos,
121145
[ProtocolType.CosmosNative]: isValidAddressCosmos,
146+
[ProtocolType.Starknet]: isValidAddressStarknet,
122147
},
123148
address,
124149
false,
@@ -153,6 +178,14 @@ export function normalizeAddressCosmos(address: Address) {
153178
}
154179
}
155180

181+
export function normalizeAddressStarknet(address: Address) {
182+
if (isZeroishAddress(address)) return address;
183+
try {
184+
return validateAndParseAddress(address);
185+
} catch {
186+
return address;
187+
}
188+
}
156189
export function normalizeAddress(address: Address, protocol?: ProtocolType) {
157190
return routeAddressUtil(
158191
{
@@ -179,6 +212,10 @@ export function eqAddressCosmos(a1: Address, a2: Address) {
179212
return normalizeAddressCosmos(a1) === normalizeAddressCosmos(a2);
180213
}
181214

215+
export function eqAddressStarknet(a1: Address, a2: Address) {
216+
return normalizeAddressStarknet(a1) === normalizeAddressStarknet(a2);
217+
}
218+
182219
export function eqAddress(a1: Address, a2: Address) {
183220
const p1 = getAddressProtocolType(a1);
184221
const p2 = getAddressProtocolType(a2);
@@ -189,6 +226,7 @@ export function eqAddress(a1: Address, a2: Address) {
189226
[ProtocolType.Sealevel]: (_a1) => eqAddressSol(_a1, a2),
190227
[ProtocolType.Cosmos]: (_a1) => eqAddressCosmos(_a1, a2),
191228
[ProtocolType.CosmosNative]: (_a1) => eqAddressCosmos(_a1, a2),
229+
[ProtocolType.Starknet]: (_a1) => eqAddressStarknet(_a1, a2),
192230
},
193231
a1,
194232
false,
@@ -208,6 +246,10 @@ export function isValidTransactionHashCosmos(input: string) {
208246
return COSMOS_TX_HASH_REGEX.test(input);
209247
}
210248

249+
export function isValidTransactionHashStarknet(input: string) {
250+
return STARKNET_TX_HASH_REGEX.test(input);
251+
}
252+
211253
export function isValidTransactionHash(input: string, protocol: ProtocolType) {
212254
if (protocol === ProtocolType.Ethereum) {
213255
return isValidTransactionHashEvm(input);
@@ -217,6 +259,8 @@ export function isValidTransactionHash(input: string, protocol: ProtocolType) {
217259
return isValidTransactionHashCosmos(input);
218260
} else if (protocol === ProtocolType.CosmosNative) {
219261
return isValidTransactionHashCosmos(input);
262+
} else if (protocol === ProtocolType.Starknet) {
263+
return isValidTransactionHashStarknet(input);
220264
} else {
221265
return false;
222266
}
@@ -226,7 +270,8 @@ export function isZeroishAddress(address: Address) {
226270
return (
227271
EVM_ZEROISH_ADDRESS_REGEX.test(address) ||
228272
SEALEVEL_ZEROISH_ADDRESS_REGEX.test(address) ||
229-
COSMOS_ZEROISH_ADDRESS_REGEX.test(address)
273+
COSMOS_ZEROISH_ADDRESS_REGEX.test(address) ||
274+
STARKNET_ZEROISH_ADDRESS_REGEX.test(address)
230275
);
231276
}
232277

@@ -271,6 +316,11 @@ export function addressToBytesCosmos(address: Address): Uint8Array {
271316
return fromBech32(address).data;
272317
}
273318

319+
export function addressToBytesStarknet(address: Address): Uint8Array {
320+
const normalizedAddress = validateAndParseAddress(address);
321+
return num.hexToBytes(normalizedAddress);
322+
}
323+
274324
export function addressToBytes(
275325
address: Address,
276326
protocol?: ProtocolType,
@@ -281,6 +331,7 @@ export function addressToBytes(
281331
[ProtocolType.Sealevel]: addressToBytesSol,
282332
[ProtocolType.Cosmos]: addressToBytesCosmos,
283333
[ProtocolType.CosmosNative]: addressToBytesCosmos,
334+
[ProtocolType.Starknet]: addressToBytesStarknet,
284335
},
285336
address,
286337
new Uint8Array(),
@@ -349,6 +400,11 @@ export function bytesToAddressCosmos(
349400
return toBech32(prefix, bytes);
350401
}
351402

403+
export function bytesToAddressStarknet(bytes: Uint8Array): Address {
404+
const hexString = encode.buf2hex(bytes);
405+
return addAddressPadding(hexString);
406+
}
407+
352408
export function bytesToProtocolAddress(
353409
bytes: Uint8Array,
354410
toProtocol: ProtocolType,
@@ -366,6 +422,8 @@ export function bytesToProtocolAddress(
366422
return bytesToAddressCosmos(bytes, prefix!);
367423
} else if (toProtocol === ProtocolType.CosmosNative) {
368424
return bytesToAddressCosmos(bytes, prefix!);
425+
} else if (toProtocol === ProtocolType.Starknet) {
426+
return bytesToAddressStarknet(bytes);
369427
} else {
370428
throw new Error(`Unsupported protocol for address ${toProtocol}`);
371429
}

typescript/utils/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ export {
55
addressToBytesCosmos,
66
addressToBytesEvm,
77
addressToBytesSol,
8+
addressToBytesStarknet,
89
bytes32ToAddress,
910
bytesToAddressCosmos,
1011
bytesToAddressEvm,
1112
bytesToAddressSol,
13+
bytesToAddressStarknet,
1214
bytesToProtocolAddress,
1315
capitalizeAddress,
1416
convertToProtocolAddress,
@@ -17,25 +19,30 @@ export {
1719
eqAddressCosmos,
1820
eqAddressEvm,
1921
eqAddressSol,
22+
eqAddressStarknet,
2023
getAddressProtocolType,
2124
isAddress,
2225
isAddressCosmos,
2326
isAddressEvm,
2427
isAddressSealevel,
28+
isAddressStarknet,
2529
isValidAddress,
2630
isValidAddressCosmos,
2731
isValidAddressEvm,
2832
isValidAddressSealevel,
33+
isValidAddressStarknet,
2934
isPrivateKeyEvm,
3035
isValidTransactionHash,
3136
isValidTransactionHashCosmos,
3237
isValidTransactionHashEvm,
3338
isValidTransactionHashSealevel,
39+
isValidTransactionHashStarknet,
3440
isZeroishAddress,
3541
normalizeAddress,
3642
normalizeAddressCosmos,
3743
normalizeAddressEvm,
3844
normalizeAddressSealevel,
45+
normalizeAddressStarknet,
3946
padBytesToLength,
4047
shortenAddress,
4148
strip0x,

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8021,6 +8021,7 @@ __metadata:
80218021
pino: "npm:^8.19.0"
80228022
prettier: "npm:^3.5.3"
80238023
sinon: "npm:^13.0.2"
8024+
starknet: "npm:^6.24.1"
80248025
typescript: "npm:5.3.3"
80258026
yaml: "npm:2.4.5"
80268027
languageName: unknown

0 commit comments

Comments
 (0)