Skip to content

Commit 4deea13

Browse files
committed
fix(projection): do not throw when encountering a handle with invalid name
1 parent 5e1fcc2 commit 4deea13

File tree

6 files changed

+132
-54
lines changed

6 files changed

+132
-54
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { Asset, Cardano, Handle } from '@cardano-sdk/core';
2-
import { InvalidStringError } from '@cardano-sdk/util';
2+
import { Logger } from 'ts-log';
33

44
/** Up to 100k transactions per block. Fits in 64-bit signed integer. */
55
export const computeCompactTxId = (blockHeight: number, txIndex: number) => blockHeight * 100_000 + txIndex;
66

7-
export const assetNameToUTF8Handle = (assetName: Cardano.AssetName): Handle => {
7+
export const assetNameToUTF8Handle = (assetName: Cardano.AssetName, logger: Logger): Handle | null => {
88
const handle = Cardano.AssetName.toUTF8(assetName);
9-
if (!Asset.util.isValidHandle(handle)) throw new InvalidStringError(`Invalid handle ${handle}`);
9+
if (!Asset.util.isValidHandle(handle)) {
10+
logger.warn(`Invalid handle: '${handle}' / '${assetName}'`);
11+
return null;
12+
}
1013
return handle;
1114
};

packages/projection/src/operators/Mappers/withHandleMetadata.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ const getHandleMetadata = (
7373
.map(({ nftMetadata, userTokenAssetId, referenceTokenAssetId, extra }): HandleMetadata | undefined => {
7474
const cip67Asset = referenceTokenAssetId && cip67Assets.byAssetId[referenceTokenAssetId];
7575
const handle = cip67Asset
76-
? assetNameToUTF8Handle(cip67Asset!.decoded.content)
77-
: assetNameToUTF8Handle(Cardano.AssetId.getAssetName(userTokenAssetId));
76+
? assetNameToUTF8Handle(cip67Asset!.decoded.content, logger)
77+
: assetNameToUTF8Handle(Cardano.AssetId.getAssetName(userTokenAssetId), logger);
7878
if (!handle) return;
7979
return {
8080
handle,

packages/projection/src/operators/Mappers/withHandles.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,18 @@ export interface WithHandles {
2727
handles: HandleOwnership[];
2828
}
2929

30-
const assetIdToUTF8Handle = (assetId: Cardano.AssetId, cip67Asset: CIP67Asset | undefined) => {
30+
const assetIdToUTF8Handle = (assetId: Cardano.AssetId, cip67Asset: CIP67Asset | undefined, logger: Logger) => {
3131
if (cip67Asset) {
3232
if (
3333
cip67Asset.decoded.label === Asset.AssetNameLabelNum.UserNFT ||
3434
cip67Asset.decoded.label === Asset.AssetNameLabelNum.VirtualHandle
3535
) {
36-
return Cardano.AssetName.toUTF8(cip67Asset.decoded.content);
36+
return assetNameToUTF8Handle(cip67Asset.decoded.content, logger);
3737
}
3838
// Ignore all but UserNFT cip67 assets
3939
return null;
4040
}
41-
return assetNameToUTF8Handle(Cardano.AssetId.getAssetName(assetId));
41+
return assetNameToUTF8Handle(Cardano.AssetId.getAssetName(assetId), logger);
4242
};
4343

4444
const getHandleMetadata = (handleDataFields: Cardano.PlutusList, logger: Logger) => {
@@ -83,7 +83,8 @@ const tryCreateHandleOwnership = (
8383
try {
8484
const cip67Asset = cip67Assets.byAssetId[assetId];
8585

86-
const handle = assetIdToUTF8Handle(assetId, cip67Asset);
86+
const handle = assetIdToUTF8Handle(assetId, cip67Asset, logger);
87+
if (!handle) return;
8788
const subhandleProps: Partial<HandleOwnership> = {};
8889
if (handle) {
8990
if (handle.includes('@')) {

packages/projection/test/operators/Mappers/handleUtil.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const maryAddress = Cardano.PaymentAddress(
1717
export const bobHandleOne = 'bob.handle.one';
1818
export const bobHandleTwo = 'bob.handle.two';
1919
export const maryHandleOne = 'mary.handle.one';
20+
export const invalidHandle = '@#!';
2021
export const virtualHandle = 'virtual@handl';
2122
export const NFTHandle = 'sub@handl';
2223
export const handleOutputs = {

packages/projection/test/operators/Mappers/withHandleMetadata.test.ts

+50-35
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { Cardano } from '@cardano-sdk/core';
1+
import { Cardano, Handle } from '@cardano-sdk/core';
22
import { ProjectionEvent } from '../../../src';
33
import {
44
assetIdFromHandle,
55
handleAssetName,
66
handleOutputs,
77
handlePolicyId,
8+
invalidHandle,
89
maryHandleOne,
910
referenceNftOutput,
1011
userNftOutput
@@ -29,43 +30,45 @@ const project = (tx: Cardano.OnChainTx) =>
2930
)
3031
);
3132

33+
const createCip25HandleMetadata = (handle: Handle) => ({
34+
blob: new Map([
35+
[
36+
721n,
37+
new Map<Cardano.Metadatum, Cardano.Metadatum>([
38+
[
39+
handlePolicyId,
40+
new Map([
41+
[
42+
handleAssetName(handle),
43+
new Map<Cardano.Metadatum, Cardano.Metadatum>([
44+
['name', `$${handle}`],
45+
['description', 'The Handle Standard'],
46+
['website', 'https://adahandle.com'],
47+
['image', 'ipfs://QmZqUk6nGqYJZzHiCGzbzqppA5qE99yNkuTSHuRQpymE1X'],
48+
[
49+
'core',
50+
new Map<Cardano.Metadatum, Cardano.Metadatum>([
51+
['og', 1n],
52+
['termsofuse', 'https://adahandle.com/tou'],
53+
['handleEncoding', 'utf-8'],
54+
['prefix', '$'],
55+
['version', 0n]
56+
])
57+
],
58+
['augmentations', []]
59+
])
60+
]
61+
])
62+
]
63+
])
64+
]
65+
])
66+
});
67+
3268
describe('withHandleMetadata', () => {
3369
it('maps "og" when handle is minted with cip25 metadata', async () => {
3470
const { handleMetadata } = await project({
35-
auxiliaryData: {
36-
blob: new Map([
37-
[
38-
721n,
39-
new Map<Cardano.Metadatum, Cardano.Metadatum>([
40-
[
41-
handlePolicyId,
42-
new Map([
43-
[
44-
handleAssetName(maryHandleOne),
45-
new Map<Cardano.Metadatum, Cardano.Metadatum>([
46-
['name', '$mary'],
47-
['description', 'The Handle Standard'],
48-
['website', 'https://adahandle.com'],
49-
['image', 'ipfs://QmZqUk6nGqYJZzHiCGzbzqppA5qE99yNkuTSHuRQpymE1X'],
50-
[
51-
'core',
52-
new Map<Cardano.Metadatum, Cardano.Metadatum>([
53-
['og', 1n],
54-
['termsofuse', 'https://adahandle.com/tou'],
55-
['handleEncoding', 'utf-8'],
56-
['prefix', '$'],
57-
['version', 0n]
58-
])
59-
],
60-
['augmentations', []]
61-
])
62-
]
63-
])
64-
]
65-
])
66-
]
67-
])
68-
},
71+
auxiliaryData: createCip25HandleMetadata(maryHandleOne),
6972
body: {
7073
mint: new Map([[assetIdFromHandle(maryHandleOne), 1n]]),
7174
outputs: [handleOutputs.oneHandleMary]
@@ -77,6 +80,18 @@ describe('withHandleMetadata', () => {
7780
expect(handleMetadata[0].txOut).toBeUndefined();
7881
});
7982

83+
it('ignores handles with invalid name', async () => {
84+
const { handleMetadata } = await project({
85+
auxiliaryData: createCip25HandleMetadata(invalidHandle),
86+
body: {
87+
mint: new Map([[assetIdFromHandle(invalidHandle), 1n]]),
88+
outputs: [handleOutputs.oneHandleMary]
89+
}
90+
} as Cardano.OnChainTx);
91+
92+
expect(handleMetadata).toHaveLength(0);
93+
});
94+
8095
describe('cip68', () => {
8196
it('maps metadata fields when only reference token is present', async () => {
8297
const { handleMetadata } = await project({

packages/projection/test/operators/Mappers/withHandles.test.ts

+68-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { Asset, Cardano } from '@cardano-sdk/core';
22
import { Buffer } from 'buffer';
3+
import { CIP67Assets, withCIP67, withHandles, withMint, withUtxo } from '../../../src/operators/Mappers';
34
import { Mappers, ProjectionEvent } from '../../../src';
45
import {
56
NFTSubHandleOutput,
67
assetIdFromHandle,
78
bobAddress,
89
bobHandleOne,
910
bobHandleTwo,
11+
handleDatum,
1012
handleOutputs,
1113
handlePolicyId,
14+
invalidHandle,
1215
maryAddress,
1316
maryHandleOne,
1417
referenceNftOutput,
@@ -19,7 +22,6 @@ import {
1922
} from './handleUtil';
2023
import { firstValueFrom, of } from 'rxjs';
2124
import { logger, mockProviders } from '@cardano-sdk/util-dev';
22-
import { withCIP67, withHandles, withMint, withUtxo } from '../../../src/operators/Mappers';
2325

2426
type In = Mappers.WithMint & Mappers.WithCIP67 & Mappers.WithNftMetadata;
2527

@@ -174,8 +176,23 @@ describe('withHandles', () => {
174176
});
175177

176178
describe('assets with invalid asset names', () => {
177-
const invalidAssetName = Asset.AssetNameLabel.encode(Cardano.AssetName('abc'), Asset.AssetNameLabelNum.UserFT);
179+
const invalidAssetName = Cardano.AssetName(Buffer.from(invalidHandle, 'utf8').toString('hex'));
178180
const invalidAssetId = Cardano.AssetId.fromParts(handlePolicyId, invalidAssetName);
181+
const decodedInvalidAssetName = Cardano.AssetName(Buffer.from(`${invalidHandle}other`, 'utf8').toString('hex'));
182+
const invalidAssetCip67AssetName = Asset.AssetNameLabel.encode(
183+
decodedInvalidAssetName,
184+
Asset.AssetNameLabelNum.UserNFT
185+
);
186+
const invalidAssetCip67ReferenceNftAssetName = Asset.AssetNameLabel.encode(
187+
decodedInvalidAssetName,
188+
Asset.AssetNameLabelNum.ReferenceNFT
189+
);
190+
const invalidCip67AssetId = Cardano.AssetId.fromParts(handlePolicyId, invalidAssetCip67AssetName);
191+
const invalidCip67ReferenceNftAssetId = Cardano.AssetId.fromParts(
192+
handlePolicyId,
193+
invalidAssetCip67ReferenceNftAssetName
194+
);
195+
179196
const outputsWithInvalidHandles = {
180197
invalidAssetName: {
181198
address: 'addr_test1vptwv4jvaqt635jvthpa29lww3vkzypm8l6vk4lv4tqfhhgajdgwf',
@@ -184,17 +201,58 @@ describe('withHandles', () => {
184201
coins: 1n
185202
}
186203
},
187-
oneValidAndOneInvalidAssetName: {
188-
address: 'addr_test1vptwv4jvaqt635jvthpa29lww3vkzypm8l6vk4lv4tqfhhgajdgwf',
204+
oneValidTwoInvalidAssetName: {
205+
address: Cardano.PaymentAddress('addr_test1vptwv4jvaqt635jvthpa29lww3vkzypm8l6vk4lv4tqfhhgajdgwf'),
206+
datum: handleDatum,
189207
value: {
190208
assets: new Map([
191209
[invalidAssetId, 1n],
192-
[assetIdFromHandle(bobHandleTwo), 1n]
193-
])
210+
[assetIdFromHandle(bobHandleTwo), 1n],
211+
[invalidCip67AssetId, 1n],
212+
[invalidCip67ReferenceNftAssetId, 1n]
213+
]),
214+
coins: 123n
194215
}
195216
}
196217
};
197218

219+
const txId = Cardano.TransactionId('0000000000000000000000000000000000000000000000000000000000000000');
220+
const utxo: [Cardano.TxIn, Cardano.TxOut] = [
221+
{ index: 0, txId },
222+
outputsWithInvalidHandles.oneValidTwoInvalidAssetName
223+
];
224+
const userNftCip67Asset = {
225+
assetId: invalidCip67ReferenceNftAssetId,
226+
assetName: invalidAssetCip67ReferenceNftAssetName,
227+
decoded: {
228+
content: decodedInvalidAssetName,
229+
label: Asset.AssetNameLabelNum.ReferenceNFT
230+
},
231+
policyId: handlePolicyId,
232+
utxo
233+
};
234+
const referenceNftCip67Asset = {
235+
assetId: invalidCip67AssetId,
236+
assetName: invalidAssetCip67AssetName,
237+
decoded: {
238+
content: decodedInvalidAssetName,
239+
label: Asset.AssetNameLabelNum.UserNFT
240+
},
241+
policyId: handlePolicyId,
242+
utxo
243+
};
244+
245+
const cip67: CIP67Assets = {
246+
byAssetId: {
247+
[invalidAssetCip67ReferenceNftAssetName]: userNftCip67Asset,
248+
[invalidCip67AssetId]: referenceNftCip67Asset
249+
},
250+
byLabel: {
251+
[Asset.AssetNameLabelNum.UserNFT]: [userNftCip67Asset],
252+
[Asset.AssetNameLabelNum.ReferenceNFT]: [referenceNftCip67Asset]
253+
}
254+
};
255+
198256
it('it returns no handles when output only contain invalid assetId', async () => {
199257
const validTxSource$ = of({
200258
block: {
@@ -222,12 +280,12 @@ describe('withHandles', () => {
222280
body: [
223281
{
224282
body: {
225-
outputs: [outputsWithInvalidHandles.oneValidAndOneInvalidAssetName]
283+
outputs: [outputsWithInvalidHandles.oneValidTwoInvalidAssetName]
226284
}
227285
}
228286
]
229287
},
230-
cip67: { byAssetId: {}, byLabel: {} }
288+
cip67
231289
} as ProjectionEvent<In>);
232290

233291
const { handles } = await firstValueFrom(
@@ -243,7 +301,7 @@ describe('withHandles', () => {
243301
body: [
244302
{
245303
body: {
246-
outputs: [outputsWithInvalidHandles.oneValidAndOneInvalidAssetName]
304+
outputs: [outputsWithInvalidHandles.oneValidTwoInvalidAssetName]
247305
}
248306
},
249307
{
@@ -253,7 +311,7 @@ describe('withHandles', () => {
253311
}
254312
]
255313
},
256-
cip67: { byAssetId: {}, byLabel: {} }
314+
cip67
257315
} as ProjectionEvent<In>);
258316

259317
const { handles } = await firstValueFrom(

0 commit comments

Comments
 (0)