Skip to content

Commit 7833c60

Browse files
add validation for cardano testnet, avalanche and beacon addresses (#3021)
1 parent 6c47a6a commit 7833c60

File tree

3 files changed

+211
-7
lines changed

3 files changed

+211
-7
lines changed

.changeset/green-melons-rush.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/proof-of-reserves-adapter': patch
3+
---
4+
5+
Add validation for cardano testnet, avalanche and ETH beacon addresses

packages/composites/proof-of-reserves/src/utils/addressValidator.ts

+85-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ type AddressObject = {
1212
const indexerToNetwork: Record<string, string> = {
1313
ada_balance: 'cardano',
1414
eth_balance: 'ethereum',
15+
eth_beacon: 'beacon',
16+
avalanche_platform: 'avalanche',
1517
bitcoin_json_rpc: 'bitcoin',
1618
lotus: 'filecoin',
1719
}
@@ -44,9 +46,12 @@ export const validateAddresses = (
4446
): AddressObject[] => {
4547
const validatedAddresses: AddressObject[] = []
4648
for (const addressObj of addresses) {
47-
const { address, network } = addressObj
49+
const { address, network, chainId } = addressObj
4850
let validatedAddress: string | undefined = undefined
49-
const validationNetwork = network || indexerToNetwork[indexer]
51+
let validationNetwork = network || indexerToNetwork[indexer]
52+
// If the indexer is eth_beacon, override the validationNetwork as it might contain a different value (goerli, ethereum, mainnet...)
53+
validationNetwork = indexer === 'eth_beacon' ? 'beacon' : validationNetwork
54+
5055
switch (validationNetwork.toLowerCase()) {
5156
case 'ethereum':
5257
validatedAddress = getValidEvmAddress(id, address)
@@ -58,11 +63,18 @@ export const validateAddresses = (
5863
validatedAddress = getValidDogeAddress(id, address)
5964
break
6065
case 'cardano':
61-
validatedAddress = getValidCardanoAddress(id, address)
66+
validatedAddress = getValidCardanoAddress(id, address, chainId)
6267
break
6368
case 'filecoin':
6469
validatedAddress = getValidFilecoinAddress(id, address)
6570
break
71+
case 'beacon':
72+
validatedAddress = getValidBeaconValidatorAddress(id, address)
73+
break
74+
case 'avalanche':
75+
case 'avalanche-fuji':
76+
validatedAddress = getValidAvalancheAddress(id, address)
77+
break
6678
default:
6779
Logger.debug(
6880
`JobId ${id}: There is no address validation procedure defined for the "${network}" network.`,
@@ -138,13 +150,32 @@ const getValidDogeAddress = (id: string, address: string): string | undefined =>
138150
return
139151
}
140152

141-
const getValidCardanoAddress = (id: string, address: string): string | undefined => {
142-
if (address.slice(0, 4).toLowerCase() === 'addr' && isBech32(address.slice(5).toLowerCase()))
153+
const getValidCardanoAddress = (
154+
id: string,
155+
address: string,
156+
chain = 'mainnet',
157+
): string | undefined => {
158+
if (chain === 'mainnet') {
159+
// Validation for 'shelley' addresses
160+
if (address.slice(0, 4).toLowerCase() === 'addr' && isBech32(address.slice(5).toLowerCase())) {
161+
return address.toLowerCase()
162+
}
163+
// Validation for legacy 'byron' addresses
164+
if (isBase58(address)) {
165+
return address
166+
}
167+
}
168+
// Validation for testnet addresses
169+
else if (
170+
chain === 'testnet' &&
171+
address.slice(0, 10).toLowerCase() === 'addr_test1' &&
172+
isBech32(address.slice(10).toLowerCase())
173+
) {
143174
return address.toLowerCase()
144-
if (isBase58(address)) return address
175+
}
145176
Logger.warn(
146177
{ warning: 'Invalid address detected' },
147-
`JobId ${id}: The address "${address}" is not a valid Dogecoin address and has been removed.`,
178+
`JobId ${id}: The address "${address}" is not a valid Cardano ${chain} address and has been removed.`,
148179
)
149180
return
150181
}
@@ -170,6 +201,53 @@ const getValidFilecoinAddress = (id: string, address: string): string | undefine
170201
return
171202
}
172203

204+
const getValidAvalancheAddress = (id: string, address: string): string | undefined => {
205+
if (address.substring(0, 2) === '0x') {
206+
// Validation for C-chain address
207+
return getValidEvmAddress(id, address)
208+
}
209+
210+
// Validation for P nd X chain addresses
211+
if (!address.startsWith('X-') && !address.startsWith('P-')) {
212+
Logger.warn(
213+
{ warning: 'Invalid address detected' },
214+
`JobId ${id}: The address "${address}" is not a valid Avalanche address and has been removed.`,
215+
)
216+
return
217+
}
218+
219+
const addressKey = address.substring(2)
220+
const addressParts = addressKey.split('1')
221+
const [, bech32Part] = [addressParts.shift() as string, addressParts.join('1')]
222+
if (!bech32Part.length || !isBech32(bech32Part.toLowerCase())) {
223+
Logger.warn(
224+
{ warning: 'Invalid address detected' },
225+
`JobId ${id}: The address "${address}" is not a valid Avalanche address and has been removed.`,
226+
)
227+
return
228+
}
229+
230+
return address
231+
}
232+
233+
const getValidBeaconValidatorAddress = (id: string, address: string): string | undefined => {
234+
try {
235+
const parsedKey = utils.hexlify(address)
236+
// Validator address is 48 bytes + '0x' prefix
237+
if (parsedKey === '0x'.padEnd(98, '0') || !utils.isHexString(parsedKey, 48)) {
238+
throw 'Invalid Address'
239+
}
240+
} catch (e) {
241+
Logger.warn(
242+
{ warning: 'Invalid address detected' },
243+
`JobId ${id}: The address "${address}" is not a valid Beacon validator address and has been removed.`,
244+
)
245+
return
246+
}
247+
248+
return address
249+
}
250+
173251
export const filterDuplicates = (id: string, addresses: AddressObject[]): AddressObject[] => {
174252
const uniqueMap: Record<string, boolean> = {}
175253
const uniqueAddresses: AddressObject[] = []

packages/composites/proof-of-reserves/test/unit/addressValidator.test.ts

+121
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,35 @@ describe('Validates Cardano addresses', () => {
196196
]
197197
expect(validateAddresses('1', 'cardano', invalidBech32Address)).toEqual([])
198198
})
199+
200+
it('Validates testnet addresses', () => {
201+
const validTestnetAddress = [
202+
{
203+
address:
204+
'addr_test1qz87tn9yat3xfutzds43tnj8qw457hk3v46w4028rtnx56v89wjwnrwcvlfm2atvcnnclh3x7thwrl7pgnffaw24mgws0dga4m',
205+
network: 'cardano',
206+
chainId: 'testnet',
207+
},
208+
{
209+
address:
210+
'addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3',
211+
network: 'cardano',
212+
chainId: 'testnet',
213+
},
214+
]
215+
expect(validateAddresses('1', 'ada_balance', validTestnetAddress)).toEqual(validTestnetAddress)
216+
})
217+
218+
it('Does not validate invalid testnet addresses', () => {
219+
const invalidTestnetAddress = [
220+
{
221+
address: 'addr_test1vpu5Olrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w',
222+
network: 'cardano',
223+
chainId: 'testnet',
224+
},
225+
]
226+
expect(validateAddresses('1', 'ada_balance', invalidTestnetAddress)).toEqual([])
227+
})
199228
})
200229

201230
describe('Validates Dogecoin addresses', () => {
@@ -278,3 +307,95 @@ describe('Validates Filecoin addresses', () => {
278307
expect(validateAddresses('1', 'filecoin', invalidAddresses)).toEqual([])
279308
})
280309
})
310+
311+
describe('Validates Beacon validator addresses', () => {
312+
it('Validates valid address', () => {
313+
const validAddresses = [
314+
{
315+
address:
316+
'0x89c1fa47be0aff073afe98720d9524534bd7cd6fdaf065e205cd290766c044eb0c39a5ed7977f7e7f3747ba550e0b0e4',
317+
network: 'mainnet',
318+
chainId: '1',
319+
},
320+
{
321+
address:
322+
'0x8000909f19cc28cd4335da3fe5dd3d84049259217d236a466885877cae255a3d30021aa25b50038ccf377b3e2e538284',
323+
network: 'mainnet',
324+
chainId: '1',
325+
},
326+
{
327+
address:
328+
'0x8000553d719a1b09006c80da3d6efb8227f5184512e89096e56f3b7ee6d29c45e44227415be00cfda05c0b3c85fe3048',
329+
network: 'mainnet',
330+
chainId: '1',
331+
},
332+
]
333+
expect(validateAddresses('1', 'eth_beacon', validAddresses)).toEqual(validAddresses)
334+
})
335+
336+
it('Does not validate invalid address', () => {
337+
const invalidAddresses = [
338+
{
339+
address:
340+
'0x89c1fa47be0aff073afe98720d9524534bd7cd6fdaf065e20544eb0c39a5ed7977f7e7f3747ba550e0b0e4',
341+
network: 'mainnet',
342+
chainId: '1',
343+
},
344+
{
345+
address: '1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za',
346+
network: 'mainnet',
347+
chainId: '1',
348+
},
349+
]
350+
expect(validateAddresses('1', 'eth_beacon', invalidAddresses)).toEqual([])
351+
})
352+
})
353+
354+
describe('Validates Avalanche addresses', () => {
355+
it('Validates valid address', () => {
356+
const validAddresses = [
357+
{
358+
address: 'P-avax1k9z2fnqu2nwvy23lf3ztgku6jdp22u6etv4ewc',
359+
network: 'avalanche',
360+
chainId: '43114',
361+
},
362+
{
363+
address: '0x74170Ba6590254c52d7786d3F590316c5BDE66a5',
364+
network: 'avalanche',
365+
chainId: '43114',
366+
},
367+
{
368+
address: 'X-avax1k9z2fnqu2nwvy23lf3ztgku6jdp22u6etv4ewc',
369+
network: 'avalanche',
370+
chainId: '43114',
371+
},
372+
{
373+
address: 'P-fuji1vd9sddlllrlk9fvj9lhntpw8t00lmvtnqkl2jt',
374+
network: 'avalanche-fuji',
375+
chainId: '43113',
376+
},
377+
]
378+
expect(validateAddresses('1', 'avalanche_platform', validAddresses)).toEqual(validAddresses)
379+
})
380+
381+
it('Does not validate invalid address', () => {
382+
const invalidAddresses = [
383+
{
384+
address: 'P-avax1k9z2fn1u2nwvy23lf3ztgku6jdp22u6etv4ewc',
385+
network: 'avalanche',
386+
chainId: '43114',
387+
},
388+
{
389+
address: '0x74170Ba6590254c52d73422786d3F590316c5BDE66a5',
390+
network: 'avalanche',
391+
chainId: '43114',
392+
},
393+
{
394+
address: 'Xdsfsavax1k9z2fnqu2nwvy23lf3ztgku6jdp22u6etv4ewc',
395+
network: 'avalanche',
396+
chainId: '43114',
397+
},
398+
]
399+
expect(validateAddresses('1', 'avalanche_platform', invalidAddresses)).toEqual([])
400+
})
401+
})

0 commit comments

Comments
 (0)