@bifrostwallet/assets 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ yarn add @bifrostwallet/assets --exact
22
22
 
23
23
  `<version>` is the package version and the git tag. That tag is written into the package at build time. Returned `logoURL` values are absolute. A missing record is still `null`.
24
24
 
25
- An exact version tag is immutable on jsDelivr. Cache headers last one year, and the file is stored permanently, so this pinned URL does not go stale ([Caching](https://www.jsdelivr.com/documentation#id-caching)). Self-host when you want a copy you control: `npx @bifrostwallet/assets@<version> fetch-logos ./logos` checks each file against the hash in that package. Pin the exact version and run it again on every bump.
25
+ An exact version tag is immutable on jsDelivr. Cache headers last one year, and the file is stored permanently, so this pinned URL does not go stale ([Caching](https://www.jsdelivr.com/documentation#id-caching)). Self-host when you want a copy you control. The steps are in [Self-hosting logos](#self-hosting-logos).
26
26
 
27
27
  Don't load logos from `raw.githubusercontent.com`. GitHub's raw file host is not a CDN: it rate-limits requests per IP and returns HTTP 429 under load. jsDelivr is built for this traffic.
28
28
 
@@ -43,23 +43,35 @@ Pass `baseURL` when you host the files yourself. A trailing slash is optional. T
43
43
 
44
44
  ### Optional: check a downloaded logo
45
45
 
46
- Use this when you download logos or self-host them with `fetch-logos`. `verifyLogoHash(bytes, logoHash)` is true when the SHA-256 of the decoded file bytes equals `logoHash`: 64 lowercase hex characters, no `0x` prefix. `fetchLogo(entry)` is a convenience that downloads `logoURL` and runs that check in one step.
46
+ `verifyLogoHash(bytes, logoHash)` is true when the SHA-256 of the decoded file bytes equals `logoHash`: 64 lowercase hex characters, no `0x` prefix. `fetchLogo(entry)` downloads `logoURL` and runs that check in one step. It does not retry unless you pass `retries` (the delay before the first retry is `retryDelay`, 500 ms by default, and it doubles each time).
47
47
 
48
48
  ### Self-hosting logos
49
49
 
50
- Logo files are not in the npm package. You do not need to clone the repository.
50
+ Logo files are not in the npm package. Clone this repository and check out the same version as your installed `@bifrostwallet/assets`. Tags have no `v` prefix.
51
51
 
52
- 1. Download this version's logos. The command checks each file against the hash in the installed package. A missing hash or a mismatch does not write that file, and the command exits non-zero. Do not upload the directory if it fails.
52
+ ```sh
53
+ git clone https://github.com/bifrostwallet/assets
54
+ cd assets
55
+ git checkout <version>
56
+ ```
57
+
58
+ `<version>` is the installed package version, for example `1.2.2`.
59
+
60
+ Upload only the `logo.webp` files under `data/`. Keep those relative paths (`tokens/…`, `providers/…`, `contracts/…`, `dapps/…`) so `baseURL` can join them.
61
+
62
+ Google Cloud:
53
63
 
54
64
  ```sh
55
- npx --yes @bifrostwallet/assets@<version> fetch-logos ./logos
65
+ gcloud storage rsync data gs://<your-bucket> --recursive --exclude='^(?!.*logo\.webp$).*'
56
66
  ```
57
67
 
58
- Files land at `./logos/providers/…`, `./logos/tokens/…`, `./logos/contracts/…`, and `./logos/dapps/…`.
68
+ Amazon S3:
59
69
 
60
- 2. Upload that directory to any static host or bucket. The public URL must serve those paths at its root. [Cloud Storage](https://cloud.google.com/storage/docs/uploading-objects) or [S3](https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html) can do that.
70
+ ```sh
71
+ aws s3 sync data s3://<your-bucket> --exclude '*' --include '*logo.webp'
72
+ ```
61
73
 
62
- 3. Point the package at the public URL:
74
+ Point the package at the public URL of that directory:
63
75
 
64
76
  ```typescript
65
77
  import { createAssets } from "@bifrostwallet/assets";
@@ -69,7 +81,7 @@ const assets = createAssets({
69
81
  });
70
82
  ```
71
83
 
72
- 4. When you bump `@bifrostwallet/assets`, run `fetch-logos` for the new version and upload again before you ship. The default jsDelivr URL changes with the version. A copy you host does not.
84
+ When you bump `@bifrostwallet/assets`, check out that version and upload again before you ship. The default jsDelivr URL changes with the version. A copy you host does not.
73
85
 
74
86
  ### Listed providers
75
87
 
package/index.d.ts CHANGED
@@ -186,6 +186,14 @@ export interface FetchLogoOptions {
186
186
  /** Defaults to global fetch. Tests pass a substitute. */
187
187
  fetch?: typeof fetch;
188
188
  signal?: AbortSignal;
189
+ /**
190
+ * Extra attempts after a network error or HTTP 403, 408, 429, or 5xx.
191
+ * Defaults to 0.
192
+ * A hash mismatch or a response over 256KB is never retried.
193
+ */
194
+ retries?: number;
195
+ /** Milliseconds before the first retry. Doubles on each later retry. Defaults to 500. */
196
+ retryDelay?: number;
189
197
  }
190
198
 
191
199
  /**
@@ -201,7 +209,7 @@ export declare function fetchLogo(
201
209
  /**
202
210
  * True when SHA-256 of the decoded logo bytes equals expectedHash.
203
211
  * expectedHash is 64 lowercase hex characters with no 0x prefix (catalog logoHash).
204
- * For bytes you already have, such as a file from fetch-logos.
212
+ * For bytes you already have.
205
213
  * fetchLogo calls this after a download.
206
214
  */
207
215
  export declare function verifyLogoHash(
package/index.js CHANGED
@@ -50,7 +50,7 @@ const CONTRACT_CATEGORIES = ["dex","aggregator","bridge","lending","staking","yi
50
50
  const CONTRACT_CATEGORY_LABELS = {"dex":"DEX","aggregator":"Aggregator","bridge":"Bridge","lending":"Lending","staking":"Staking","yield":"Yield","perpetuals":"Perpetuals","predictions":"Predictions","nft-marketplace":"NFT Marketplace","governance":"Governance","fassets":"FAssets","rewards":"Rewards","other":"Other"};
51
51
  const DAPP_CATEGORIES = ["featured","dex","lending","yield","staking","bridges","marketplaces","social","games","other"];
52
52
  const DAPP_CATEGORY_LABELS = {"featured":"Featured","dex":"DEX","lending":"Lending","yield":"Yield","staking":"Staking","bridges":"Bridges","marketplaces":"Marketplaces","social":"Social","games":"Games","other":"Other"};
53
- const DEFAULT_LOGO_BASE_URL = "https://cdn.jsdelivr.net/gh/bifrostwallet/assets@1.3.0/data/";
53
+ const DEFAULT_LOGO_BASE_URL = "https://cdn.jsdelivr.net/gh/bifrostwallet/assets@1.3.2/data/";
54
54
 
55
55
  const EIP712_DOMAIN_SALT = "0xffc5336f20c2026b0f53d269def3cdffa3ca99b07e238af05f263a89517104f7";
56
56
  const PROVIDER_LISTING_PRIMARY_TYPE = "ProviderListing";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bifrostwallet/assets",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Curated catalog of data providers, validators, tokens, contracts and dapps for Bifrost Wallet.",
5
5
  "repository": "https://github.com/bifrostwallet/assets.git",
6
6
  "author": "Bifrost Software Ltd",
@@ -17,9 +17,6 @@
17
17
  "default": "./signatures.js"
18
18
  }
19
19
  },
20
- "bin": {
21
- "fetch-logos": "./fetch-logos.js"
22
- },
23
20
  "sideEffects": false,
24
21
  "dependencies": {}
25
22
  }
@@ -1916,6 +1916,15 @@
1916
1916
  "logoURL": "tokens/eip155-56/0xba9425ec55ee0e72216d18e0ad8bbba2553bfb60/logo.webp",
1917
1917
  "logoHash": "935a9ac318bf75dfdf1f5a61a4162be707c624d6f43781ac91182f96180a64b1"
1918
1918
  },
1919
+ {
1920
+ "name": "Wrapped BNB",
1921
+ "symbol": "WBNB",
1922
+ "decimals": 18,
1923
+ "address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
1924
+ "coingeckoId": "wbnb",
1925
+ "logoURL": "tokens/eip155-56/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c/logo.webp",
1926
+ "logoHash": "8926a7bc9572fbc38319d019f3cd92f6eeaca99d2f7ca57358b5771856721a98"
1927
+ },
1919
1928
  {
1920
1929
  "name": "Alchemy Pay",
1921
1930
  "symbol": "ACH",
@@ -2598,6 +2607,6 @@
2598
2607
  "address": null,
2599
2608
  "coingeckoId": "binancecoin",
2600
2609
  "logoURL": "tokens/eip155-56/native/logo.webp",
2601
- "logoHash": "ef582e6a8c2a95b3db2ae03803304c422a63aaf38a40914fbb809d625a39c461"
2610
+ "logoHash": "8926a7bc9572fbc38319d019f3cd92f6eeaca99d2f7ca57358b5771856721a98"
2602
2611
  }
2603
2612
  ]
@@ -6,6 +6,6 @@
6
6
  "address": null,
7
7
  "coingeckoId": null,
8
8
  "logoURL": "tokens/eip155-97/native/logo.webp",
9
- "logoHash": "ef582e6a8c2a95b3db2ae03803304c422a63aaf38a40914fbb809d625a39c461"
9
+ "logoHash": "8926a7bc9572fbc38319d019f3cd92f6eeaca99d2f7ca57358b5771856721a98"
10
10
  }
11
11
  ]
@@ -13,6 +13,42 @@ function verifyLogoHash(bytes, expectedHash) {
13
13
  return crypto.timingSafeEqual(digest, expected);
14
14
  }
15
15
 
16
+ function delay(ms) {
17
+ return new Promise((resolve) => {
18
+ setTimeout(resolve, ms);
19
+ });
20
+ }
21
+
22
+ function readRetryCount(value) {
23
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
24
+ const count = Math.floor(value);
25
+ return count > 0 ? count : 0;
26
+ }
27
+
28
+ function readRetryDelay(value) {
29
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return 500;
30
+ return value;
31
+ }
32
+
33
+ function isRetryableStatus(status) {
34
+ return status === 403 || status === 408 || status === 429 || (status >= 500 && status <= 599);
35
+ }
36
+
37
+ function callerAborted(options) {
38
+ return !!(options && options.signal && options.signal.aborted);
39
+ }
40
+
41
+ async function backoff(options, attempt, status) {
42
+ const retries = readRetryCount(options && options.retries);
43
+ const retryDelay = readRetryDelay(options && options.retryDelay);
44
+ const wait = retryDelay * 2 ** (attempt - 1);
45
+ const onRetry = options && options.onRetry;
46
+ if (typeof onRetry === "function") {
47
+ await onRetry({ attempt, retries, status, delay: wait });
48
+ }
49
+ if (wait > 0) await delay(wait);
50
+ }
51
+
16
52
  async function fetchLogo(entry, options) {
17
53
  if (!entry || typeof entry.logoURL !== "string" || entry.logoURL.trim() === "") {
18
54
  throw new Error("logoURL is required");
@@ -21,29 +57,62 @@ async function fetchLogo(entry, options) {
21
57
  throw new Error("logoHash must be a lowercase SHA-256 hex digest");
22
58
  }
23
59
  const fetchImpl = (options && options.fetch) || fetch;
24
- const signal = options && options.signal ? options.signal : AbortSignal.timeout(30000);
25
- let response;
26
- try {
27
- response = await fetchImpl(entry.logoURL, {
28
- redirect: "follow",
29
- headers: { accept: "application/octet-stream" },
30
- signal,
31
- });
32
- } catch (error) {
33
- const message = error && error.message ? error.message : String(error);
34
- throw new Error("Could not fetch logo: " + message);
35
- }
36
- if (!response || !response.ok) {
37
- const status = response && response.status ? response.status : "request failed";
38
- throw new Error("Could not fetch logo: HTTP " + status);
39
- }
40
- const bytes = new Uint8Array(await response.arrayBuffer());
41
- if (bytes.byteLength > 256 * 1024) {
42
- throw new Error("Could not fetch logo: response exceeds 256KB");
43
- }
44
- if (!verifyLogoHash(bytes, entry.logoHash)) {
45
- throw new Error("logo hash mismatch");
60
+ const retries = readRetryCount(options && options.retries);
61
+ let attempt = 0;
62
+ for (;;) {
63
+ const signal = options && options.signal ? options.signal : AbortSignal.timeout(30000);
64
+ let response;
65
+ try {
66
+ response = await fetchImpl(entry.logoURL, {
67
+ redirect: "follow",
68
+ headers: { accept: "application/octet-stream" },
69
+ signal,
70
+ });
71
+ } catch (error) {
72
+ if (!callerAborted(options) && attempt < retries) {
73
+ attempt += 1;
74
+ await backoff(options, attempt, "network");
75
+ continue;
76
+ }
77
+ const message = error && error.message ? error.message : String(error);
78
+ throw new Error("Could not fetch logo: " + message);
79
+ }
80
+ if (!response || !response.ok) {
81
+ const status = response && typeof response.status === "number" ? response.status : 0;
82
+ if (response && typeof response.arrayBuffer === "function") {
83
+ try {
84
+ await response.arrayBuffer();
85
+ } catch (drainError) {
86
+ void drainError;
87
+ }
88
+ }
89
+ if (isRetryableStatus(status) && attempt < retries && !callerAborted(options)) {
90
+ attempt += 1;
91
+ await backoff(options, attempt, status);
92
+ continue;
93
+ }
94
+ const label = status ? status : "request failed";
95
+ throw new Error("Could not fetch logo: HTTP " + label);
96
+ }
97
+ let bytes;
98
+ try {
99
+ bytes = new Uint8Array(await response.arrayBuffer());
100
+ } catch (error) {
101
+ if (!callerAborted(options) && attempt < retries) {
102
+ attempt += 1;
103
+ await backoff(options, attempt, "network");
104
+ continue;
105
+ }
106
+ const message = error && error.message ? error.message : String(error);
107
+ throw new Error("Could not fetch logo: " + message);
108
+ }
109
+ if (bytes.byteLength > 256 * 1024) {
110
+ throw new Error("Could not fetch logo: response exceeds 256KB");
111
+ }
112
+ if (!verifyLogoHash(bytes, entry.logoHash)) {
113
+ throw new Error("logo hash mismatch");
114
+ }
115
+ return bytes;
46
116
  }
47
- return bytes;
48
117
  }
49
118
  module.exports = { verifyLogoHash, fetchLogo };
package/fetch-logos.js DELETED
@@ -1,193 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- // @bifrostwallet/assets fetch-logos — generated file, do not edit.
4
- // Downloads logo files for this package version. Each catalog entry's logoHash
5
- // is the trust anchor. Bytes are the decoded response body (content-encoding removed).
6
- // Accept is application/octet-stream so the CDN is not asked to convert the image.
7
-
8
- const fs = require("node:fs");
9
- const path = require("node:path");
10
- const { fetchLogo, verifyLogoHash } = require("./verify-logo-hash.js");
11
-
12
- const DEFAULT_LOGO_BASE_URL = "https://cdn.jsdelivr.net/gh/bifrostwallet/assets@1.3.0/data/";
13
- const USAGE = [
14
- "Usage: fetch-logos <dir>",
15
- "",
16
- "Download this package version's logos into <dir>.",
17
- "Paths stay relative to data/ (providers/, tokens/, contracts/, dapps/).",
18
- "Each file is checked against the logoHash on its catalog entry.",
19
- "A missing hash or a hash mismatch does not write that file, and the command exits non-zero.",
20
- "Do not upload the directory when the command fails.",
21
- "",
22
- "Upload <dir> to any static host. createAssets baseURL is optional.",
23
- "Omit it to use jsDelivr at this release tag:",
24
- "https://cdn.jsdelivr.net/gh/bifrostwallet/assets@1.3.0/data/",
25
- "Or set baseURL to the public URL where you uploaded that directory.",
26
- "Example: https://assets.example.com/",
27
- "Run fetch-logos again when you bump the package version.",
28
- ].join("\n");
29
-
30
- function readJson(filePath) {
31
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
32
- }
33
-
34
- function walkJsonArrays(dir, visit) {
35
- if (!fs.existsSync(dir)) return;
36
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
37
- const fullPath = path.join(dir, entry.name);
38
- if (entry.isDirectory()) {
39
- walkJsonArrays(fullPath, visit);
40
- continue;
41
- }
42
- if (!entry.name.endsWith(".json")) continue;
43
- if (entry.name === "package.json" || entry.name === "signatures.json") {
44
- continue;
45
- }
46
- const parsed = readJson(fullPath);
47
- if (!Array.isArray(parsed)) continue;
48
- for (const item of parsed) {
49
- if (item && typeof item === "object" && typeof item.logoURL === "string") {
50
- visit(item, path.relative(dir, fullPath));
51
- }
52
- }
53
- }
54
- }
55
-
56
- function isSafeLogoPath(value) {
57
- if (!value || value.startsWith("/") || value.includes("..") || value.includes("\\")) return false;
58
- if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return false;
59
- return /^(providers|tokens|contracts|dapps)\/[A-Za-z0-9._@/-]+$/.test(value);
60
- }
61
-
62
- function loadAnchors(packageRoot) {
63
- const entries = new Map();
64
- const failures = [];
65
- walkJsonArrays(packageRoot, (item) => {
66
- const logoPath = item.logoURL;
67
- const logoHash = item.logoHash;
68
- if (!isSafeLogoPath(logoPath)) {
69
- failures.push(String(logoPath) + ": invalid logo path");
70
- return;
71
- }
72
- if (typeof logoHash !== "string" || !/^[0-9a-f]{64}$/.test(logoHash)) {
73
- failures.push(logoPath + ": missing logoHash");
74
- entries.set(logoPath, null);
75
- return;
76
- }
77
- const existing = entries.get(logoPath);
78
- if (existing !== undefined && existing !== logoHash) {
79
- failures.push(logoPath + ": logoHash conflict");
80
- entries.set(logoPath, null);
81
- return;
82
- }
83
- if (existing === undefined) entries.set(logoPath, logoHash);
84
- });
85
- return { entries, failures };
86
- }
87
-
88
- function destinationFor(destDir, logoPath) {
89
- const root = path.resolve(destDir);
90
- const target = path.resolve(root, logoPath);
91
- const relative = path.relative(root, target);
92
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
93
- throw new Error("logo path escapes destination");
94
- }
95
- return target;
96
- }
97
-
98
- async function mapPool(items, limit, fn) {
99
- const queue = items.slice();
100
- const workers = [];
101
- const count = Math.min(limit, queue.length);
102
- for (let i = 0; i < count; i += 1) {
103
- workers.push(
104
- (async () => {
105
- while (queue.length > 0) {
106
- const item = queue.shift();
107
- await fn(item);
108
- }
109
- })(),
110
- );
111
- }
112
- await Promise.all(workers);
113
- }
114
-
115
- async function fetchLogos(options) {
116
- if (!options || typeof options.destDir !== "string" || options.destDir.trim() === "") {
117
- throw new Error("destDir is required");
118
- }
119
- const packageRoot = options.packageRoot || __dirname;
120
- const supplied = typeof options.baseURL === "string" ? options.baseURL.trim() : "";
121
- const baseURL = (supplied || DEFAULT_LOGO_BASE_URL).replace(/\/?$/, "/");
122
- const fetchImpl = options.fetch || fetch;
123
- const loaded = loadAnchors(packageRoot);
124
- const failures = loaded.failures.slice();
125
- const written = [];
126
- const jobs = [];
127
- for (const [logoPath, logoHash] of loaded.entries) {
128
- if (typeof logoHash !== "string") continue;
129
- jobs.push({ logoPath, logoHash });
130
- }
131
- await mapPool(jobs, 8, async (job) => {
132
- try {
133
- const bytes = await fetchLogo(
134
- { logoURL: baseURL + job.logoPath, logoHash: job.logoHash },
135
- { fetch: fetchImpl },
136
- );
137
- const target = destinationFor(options.destDir, job.logoPath);
138
- fs.mkdirSync(path.dirname(target), { recursive: true });
139
- const partial = target + ".partial";
140
- fs.writeFileSync(partial, bytes);
141
- fs.renameSync(partial, target);
142
- written.push(job.logoPath);
143
- } catch (error) {
144
- const message = error && error.message ? error.message : String(error);
145
- failures.push(job.logoPath + ": " + message);
146
- }
147
- });
148
- return { written, failures };
149
- }
150
-
151
- async function runCli(argv, deps) {
152
- const args = Array.isArray(argv) ? argv.slice() : [];
153
- const extra = deps || {};
154
- if (args.includes("--help") || args.includes("-h")) {
155
- console.info(USAGE);
156
- return 0;
157
- }
158
- if (args[0] === "fetch-logos") args.shift();
159
- if (args.length !== 1) {
160
- console.error(USAGE);
161
- return 1;
162
- }
163
- const result = await fetchLogos({
164
- destDir: args[0],
165
- packageRoot: extra.packageRoot,
166
- baseURL: extra.baseURL,
167
- fetch: extra.fetch,
168
- });
169
- for (const failure of result.failures) console.error(failure);
170
- if (result.failures.length > 0) {
171
- console.error(
172
- "fetch-logos failed for " +
173
- result.failures.length +
174
- " logo(s). Those files were not written. Do not upload this directory.",
175
- );
176
- return 1;
177
- }
178
- console.info("Wrote " + result.written.length + " logo(s) to " + args[0]);
179
- return 0;
180
- }
181
-
182
- if (require.main === module) {
183
- runCli(process.argv.slice(2))
184
- .then((code) => {
185
- process.exitCode = code;
186
- })
187
- .catch((error) => {
188
- console.error(error && error.message ? error.message : error);
189
- process.exitCode = 1;
190
- });
191
- }
192
-
193
- module.exports = { fetchLogos, fetchLogo, runCli, verifyLogoHash };