Skip to content

Commit f569316

Browse files
committed
Merge remote-tracking branch 'origin/master' into fix/send-flow-asset-list-formatting
# Conflicts: # extension/e2e-tests/blockaidScan.malicious.test.ts # extension/e2e-tests/blockaidScan.safe.test.ts # extension/e2e-tests/blockaidScan.suspicious.test.ts # extension/e2e-tests/blockaidScan.unable.test.ts # extension/e2e-tests/sendPayment.test.ts
2 parents 12cbaef + 7015e62 commit f569316

194 files changed

Lines changed: 16685 additions & 2562 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Upstream SEP-0042 asset-list JSON schema, pinned verbatim for deterministic tests.
2+
/* eslint-disable */
3+
export const SEP0042_ASSETLIST_SCHEMA = {
4+
$schema: "http://json-schema.org/draft-07/schema#",
5+
title: "Stellar Asset List",
6+
description: "Schema for Stellar Asset Lists (SAL)",
7+
type: "object",
8+
definitions: {
9+
CleanString: {
10+
type: "string",
11+
pattern: "^[\\w\\u0020.,-@]*$",
12+
},
13+
},
14+
properties: {
15+
name: {
16+
$ref: "#/definitions/CleanString",
17+
maxLength: 30,
18+
minLength: 5,
19+
description: "Short descriptive title of the list",
20+
},
21+
network: {
22+
type: "string",
23+
enum: ["public", "testnet"],
24+
},
25+
provider: {
26+
$ref: "#/definitions/CleanString",
27+
maxLength: 50,
28+
minLength: 5,
29+
description: "Organization or entity that put together the list",
30+
},
31+
description: {
32+
type: "string",
33+
maxLength: 400,
34+
description:
35+
"Text description of the list to display alongside with the name",
36+
},
37+
version: {
38+
type: "string",
39+
pattern: "^\\d{1,4}\\.\\d{1,4}$",
40+
description: "Current list revision",
41+
},
42+
feedback: {
43+
type: "string",
44+
format: "uri",
45+
maxLength: 100,
46+
description:
47+
"URL or Github repository address where users can report bad actors or request addition of new assets",
48+
},
49+
assets: {
50+
type: "array",
51+
minItems: 1,
52+
maxItems: 1000,
53+
description: "Array of asset metadata entries",
54+
items: {
55+
type: "object",
56+
properties: {
57+
name: {
58+
$ref: "#/definitions/CleanString",
59+
maxLength: 30,
60+
minLength: 5,
61+
description: "Array of asset metadata entries",
62+
},
63+
contract: {
64+
type: "string",
65+
pattern: "^C[A-Z0-9]{55}$",
66+
description:
67+
"Asset contract address in StrKey encoding (for Soroban assets)",
68+
},
69+
code: {
70+
type: "string",
71+
pattern: "^[A-Za-z0-9]{1,12}$",
72+
description: "Asset code (for Classic assets)",
73+
},
74+
issuer: {
75+
type: "string",
76+
pattern: "^G[A-Z0-9]{55}$",
77+
description: "Asset issuer address (for Classic assets)",
78+
},
79+
org: {
80+
$ref: "#/definitions/CleanString",
81+
maxLength: 30,
82+
minLength: 5,
83+
description: "Issuer organization/company",
84+
},
85+
domain: {
86+
type: "string",
87+
pattern:
88+
"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$",
89+
description:
90+
"FQDN of the site that hosts asset-related stellar.toml format",
91+
},
92+
icon: {
93+
type: "string",
94+
oneOf: [
95+
{
96+
format: "uri",
97+
},
98+
{
99+
pattern: "^baf[a-zA-Z0-9]+$",
100+
},
101+
],
102+
description:
103+
"Icon URL (only HTTPS protocol is supported) or IPFS hash",
104+
},
105+
decimals: {
106+
type: "integer",
107+
minimum: 0,
108+
maximum: 38,
109+
description: "Number of decimals to display",
110+
},
111+
comment: {
112+
type: "string",
113+
maxLength: 150,
114+
description:
115+
"Alerts, messages, or other additional information specified by the provider",
116+
},
117+
},
118+
required: ["name", "org"],
119+
anyOf: [
120+
{
121+
required: ["contract"],
122+
},
123+
{
124+
required: ["code", "issuer"],
125+
},
126+
],
127+
additionalProperties: false,
128+
},
129+
},
130+
},
131+
required: ["name", "provider", "version", "assets"],
132+
additionalProperties: false,
133+
} as const;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
schemaValidatedAssetList,
3+
__resetSep0042SchemaCache,
4+
} from "../token-list";
5+
import { AssetListResponse } from "@shared/constants/soroban/asset-list";
6+
import { SEP0042_ASSETLIST_SCHEMA } from "./fixtures/sep0042-assetlist.schema";
7+
8+
jest.mock("@sentry/browser", () => ({
9+
captureException: jest.fn(),
10+
}));
11+
12+
import { captureException } from "@sentry/browser";
13+
14+
const VALID_CONTRACT = `C${"A".repeat(55)}`; // ^C[A-Z0-9]{55}$
15+
const VALID_ISSUER = `G${"A".repeat(55)}`; // ^G[A-Z0-9]{55}$
16+
17+
const baseAsset = (overrides: Record<string, unknown> = {}) => ({
18+
name: "Test Asset",
19+
org: "Test Org",
20+
code: "TEST",
21+
issuer: VALID_ISSUER,
22+
contract: VALID_CONTRACT,
23+
domain: "test.com",
24+
icon: "https://test.com/icon.png",
25+
decimals: 7,
26+
...overrides,
27+
});
28+
29+
const baseList = (overrides: Record<string, unknown> = {}): AssetListResponse =>
30+
({
31+
name: "Test List",
32+
provider: "Test Provider",
33+
description: "A test list",
34+
version: "1.0",
35+
network: "public",
36+
assets: [baseAsset()],
37+
...overrides,
38+
}) as AssetListResponse;
39+
40+
describe("schemaValidatedAssetList", () => {
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
// The schema is memoized across validations, so clear the cache between
44+
// cases (each mocks its own fetch response).
45+
__resetSep0042SchemaCache();
46+
// The SEP-0042 schema is fetched over the network; return the fixture
47+
// so validation is deterministic and offline.
48+
global.fetch = jest.fn().mockResolvedValue({
49+
ok: true,
50+
status: 200,
51+
json: async () => SEP0042_ASSETLIST_SCHEMA,
52+
}) as jest.Mock;
53+
});
54+
55+
it("accepts 'mainnet' as a network value", async () => {
56+
const result = await schemaValidatedAssetList(
57+
baseList({ network: "mainnet" }),
58+
);
59+
expect(result.errors).toBeNull();
60+
expect(result.assets).toHaveLength(1);
61+
});
62+
63+
it("accepts a three-segment version", async () => {
64+
const result = await schemaValidatedAssetList(
65+
baseList({ version: "1.4.4" }),
66+
);
67+
expect(result.errors).toBeNull();
68+
expect(result.assets).toHaveLength(1);
69+
});
70+
71+
it("strips an invalid name but keeps the asset and its other fields", async () => {
72+
const result = await schemaValidatedAssetList(
73+
baseList({ assets: [baseAsset({ name: "Carbon tCO₂e offset" })] }),
74+
);
75+
expect(result.errors).toBeNull();
76+
expect(result.assets).toHaveLength(1);
77+
expect(result.assets[0].name).toBeUndefined();
78+
expect(result.assets[0].code).toBe("TEST");
79+
expect(result.assets[0].contract).toBe(VALID_CONTRACT);
80+
});
81+
82+
it("strips an invalid contract but keeps the asset via code+issuer", async () => {
83+
const result = await schemaValidatedAssetList(
84+
baseList({
85+
assets: [baseAsset({ contract: "deadbeef-not-a-contract" })],
86+
}),
87+
);
88+
expect(result.errors).toBeNull();
89+
expect(result.assets).toHaveLength(1);
90+
expect(result.assets[0].contract).toBeUndefined();
91+
expect(result.assets[0].code).toBe("TEST");
92+
expect(result.assets[0].issuer).toBe(VALID_ISSUER);
93+
});
94+
95+
it("tolerates assets missing both name and org", async () => {
96+
const asset = baseAsset();
97+
delete (asset as Record<string, unknown>).name;
98+
delete (asset as Record<string, unknown>).org;
99+
const result = await schemaValidatedAssetList(
100+
baseList({ assets: [asset] }),
101+
);
102+
expect(result.errors).toBeNull();
103+
expect(result.assets).toHaveLength(1);
104+
});
105+
106+
it("still rejects a list with a non-relaxed violation (bad domain)", async () => {
107+
const result = await schemaValidatedAssetList(
108+
baseList({ assets: [baseAsset({ domain: "NOT A DOMAIN!!" })] }),
109+
);
110+
expect(result.assets).toHaveLength(0);
111+
expect(result.errors).not.toBeNull();
112+
expect(result.errors!.length).toBeGreaterThan(0);
113+
});
114+
115+
it("returns empty assets and null errors when the schema cannot be fetched", async () => {
116+
(global.fetch as jest.Mock).mockResolvedValue({ ok: false, status: 500 });
117+
const result = await schemaValidatedAssetList(baseList());
118+
expect(result.assets).toHaveLength(0);
119+
expect(result.errors).toBeNull();
120+
expect(captureException).toHaveBeenCalled();
121+
});
122+
123+
it("does not mutate the input list when stripping fields", async () => {
124+
const list = baseList({
125+
assets: [baseAsset({ contract: "deadbeef-not-a-contract" })],
126+
});
127+
const snapshot = JSON.stringify(list);
128+
await schemaValidatedAssetList(list);
129+
expect(JSON.stringify(list)).toBe(snapshot);
130+
});
131+
132+
it("fetches the SEP-0042 schema only once across multiple validations", async () => {
133+
await schemaValidatedAssetList(baseList());
134+
await schemaValidatedAssetList(baseList());
135+
await schemaValidatedAssetList(baseList());
136+
expect(global.fetch).toHaveBeenCalledTimes(1);
137+
});
138+
});

0 commit comments

Comments
 (0)