@carbonx3/cx3.js 0.1.1 → 0.3.0
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 +239 -51
- package/index.d.ts +108 -21
- package/index.js +14 -6
- package/package.json +13 -6
- package/src/abi.js +8 -1
- package/src/contract.js +5 -0
- package/src/crypto.js +17 -4
- package/src/digest.js +74 -38
- package/src/factory.js +105 -1
- package/src/provider.js +344 -59
- package/src/utils.js +19 -0
- package/src/wallet.js +63 -13
package/src/provider.js
CHANGED
|
@@ -1,34 +1,118 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { Cx3Error, Cx3HttpError, Cx3TxError } = require("./errors");
|
|
4
|
-
const { readCallValue, sleep, normalizeAddress, walletApiKeyMode } = require("./utils");
|
|
4
|
+
const { readCallValue, sleep, normalizeAddress, walletApiKeyMode, toJsonSafe } = require("./utils");
|
|
5
5
|
const { Contract } = require("./contract");
|
|
6
|
+
const { normalizeGenesisHash } = require("./digest");
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Green Chain HTTP provider.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
+
* Apps sign in to the CX3Api gateway with an OAuth2 client (`clientId` + `clientSecret`, created in
|
|
12
|
+
* CX3WebUI → Settings → API → Applications): the provider gets a one-hour access token at
|
|
13
|
+
* `POST /oauth/token`, sends it as `Authorization: Bearer`, and renews it before it expires. A wallet
|
|
14
|
+
* API key (`cx3wk_…`, `X-Api-Key`) still works during the move to applications. Without one of the
|
|
15
|
+
* two the provider refuses to start (CREDENTIALS_REQUIRED): no app development without credentials.
|
|
16
|
+
* Gateway routes: `/contracts/:address/call`, `/contracts/:address/send`, `/wallet/...`, …
|
|
17
|
+
* `devNode: true` talks to a development node that runs without auth (`/api/v1`). The SDK never sends
|
|
18
|
+
* HTTP Basic (the nodes' internal login). Never talks to CX3VM directly.
|
|
11
19
|
*/
|
|
20
|
+
|
|
21
|
+
// node route → CX3Api route (the gateway forwards the same body to the node)
|
|
22
|
+
function gatewayPath(path, body) {
|
|
23
|
+
const addr = body && (body.contractAddress || body.address);
|
|
24
|
+
if (path === "/contract/call" && addr) return `/contracts/${encodeURIComponent(addr)}/call`;
|
|
25
|
+
if (path === "/contract/send" && addr) return `/contracts/${encodeURIComponent(addr)}/send`;
|
|
26
|
+
if (path === "/deployContract") return "/contracts/deploy";
|
|
27
|
+
if (path === "/getBalanceAndNonce") return "/wallet/getBalanceAndNonce";
|
|
28
|
+
if (path === "/getNonce") return "/wallet/getNonce";
|
|
29
|
+
if (path === "/getContracts") return "/contracts/list";
|
|
30
|
+
const info = /^\/contract\/([^/]+)$/.exec(path);
|
|
31
|
+
if (info) return `/contracts/${info[1]}`;
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function appendQuery(url, query) {
|
|
36
|
+
if (!query) return url;
|
|
37
|
+
const qs = new URLSearchParams();
|
|
38
|
+
for (const [k, v] of Object.entries(query)) {
|
|
39
|
+
if (v !== undefined && v !== null && v !== "") qs.set(k, String(v));
|
|
40
|
+
}
|
|
41
|
+
const s = qs.toString();
|
|
42
|
+
return s ? url + (url.includes("?") ? "&" : "?") + s : url;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// a private token moves only between its owner and its authorized wallets (the templates' guards)
|
|
46
|
+
const PRIVATE_GUARDED_METHODS = new Set(["transfer", "transferFrom", "mint"]);
|
|
47
|
+
|
|
48
|
+
// a bool view answers true / false on CX3JS and "true" / "false" on CX3WASM
|
|
49
|
+
function boolVerdict(v) {
|
|
50
|
+
if (v === true || v === 1 || v === "1" || v === "true") return true;
|
|
51
|
+
if (v === false || v === 0 || v === "0" || v === "false") return false;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// credentials come only from clientId / clientSecret or apiKey, never from an extra header
|
|
56
|
+
function withoutCredentialHeaders(headers) {
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const [k, v] of Object.entries(headers || {})) {
|
|
59
|
+
if (!/^(authorization|x-api-key)$/i.test(k)) out[k] = v;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CLIENT_ID_RE = /^cx3app_[a-f0-9]{24}$/i;
|
|
65
|
+
const CLIENT_SECRET_RE = /^cx3sec_[a-f0-9]{64}$/i;
|
|
66
|
+
|
|
12
67
|
class Cx3Provider {
|
|
13
68
|
/**
|
|
14
|
-
* @param {string} baseUrl
|
|
69
|
+
* @param {string} baseUrl CX3Api gateway URL (`CX3_NODE_URL`, e.g. https://cx3api.carbonx3.com), or a development node with `devNode`
|
|
15
70
|
* @param {object} [options]
|
|
16
|
-
* @param {string} [options.
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {string} [options.
|
|
19
|
-
* @param {
|
|
71
|
+
* @param {string} [options.clientId] OAuth2 client id (`cx3app_…`; env CX3_CLIENT_ID)
|
|
72
|
+
* @param {string} [options.clientSecret] OAuth2 client secret (`cx3sec_…`; env CX3_CLIENT_SECRET)
|
|
73
|
+
* @param {string|string[]} [options.scope] ask for fewer scopes than the app has ("read", "write", "deploy")
|
|
74
|
+
* @param {string} [options.apiKey] wallet API key (`cx3wk_…`; env CX3_API_KEY), until apps move to OAuth2
|
|
75
|
+
* @param {boolean} [options.devNode] a development node without auth (`/api/v1`), no credentials
|
|
76
|
+
* @param {string} [options.apiPrefix] ("" on the gateway, "/api/v1" on a development node)
|
|
77
|
+
* @param {Record<string,string>} [options.headers] Extra headers (Authorization / X-Api-Key are dropped)
|
|
20
78
|
* @param {typeof fetch} [options.fetch] Custom fetch (tests)
|
|
21
79
|
* @param {number} [options.timeoutMs]
|
|
22
80
|
*/
|
|
23
81
|
constructor(baseUrl, options = {}) {
|
|
24
82
|
if (!baseUrl) throw new Cx3Error("Cx3Provider requires baseUrl (CX3_NODE_URL)", "NO_BASE_URL");
|
|
25
83
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
84
|
+
if (options.auth != null) {
|
|
85
|
+
throw new Cx3Error(
|
|
86
|
+
"HTTP Basic is the nodes' internal login and is not sent by the SDK: create an application in CX3WebUI (Settings → API) and pass clientId / clientSecret",
|
|
87
|
+
"BASIC_AUTH_UNSUPPORTED"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const env = (name) => (process.env[name] || "").trim();
|
|
91
|
+
this.clientId = String(options.clientId != null ? options.clientId : env("CX3_CLIENT_ID")).trim();
|
|
92
|
+
this._clientSecret = String(options.clientSecret != null ? options.clientSecret : env("CX3_CLIENT_SECRET")).trim();
|
|
93
|
+
this.scope = Array.isArray(options.scope) ? options.scope.join(" ") : String(options.scope || "").trim();
|
|
94
|
+
this.apiKey = String(options.apiKey != null ? options.apiKey : env("CX3_API_KEY")).trim();
|
|
95
|
+
this.devNode = options.devNode === true;
|
|
96
|
+
this.oauth = !!(this.clientId || this._clientSecret);
|
|
97
|
+
if (this.oauth && (!CLIENT_ID_RE.test(this.clientId) || !CLIENT_SECRET_RE.test(this._clientSecret))) {
|
|
98
|
+
throw new Cx3Error("clientId must be cx3app_… and clientSecret cx3sec_… (CX3WebUI → Settings → API → Applications)", "INVALID_CLIENT_CREDENTIALS");
|
|
99
|
+
}
|
|
100
|
+
const hasKey = walletApiKeyMode(this.apiKey) !== "public";
|
|
101
|
+
if (!this.oauth && !hasKey && !this.devNode) {
|
|
102
|
+
throw new Cx3Error(
|
|
103
|
+
"App credentials required: create an application in CX3WebUI (Settings → API → Applications) and pass clientId / clientSecret (or set CX3_CLIENT_ID / CX3_CLIENT_SECRET)",
|
|
104
|
+
"CREDENTIALS_REQUIRED"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
this.gateway = !this.devNode;
|
|
108
|
+
this.apiPrefix = options.apiPrefix != null ? options.apiPrefix : (this.gateway ? "" : "/api/v1");
|
|
109
|
+
this.extraHeaders = withoutCredentialHeaders(options.headers);
|
|
30
110
|
this.fetchImpl = options.fetch || globalThis.fetch;
|
|
31
111
|
this.timeoutMs = options.timeoutMs || 60000;
|
|
112
|
+
this._token = null; // { value, refreshAt, scope }
|
|
113
|
+
this._tokenPromise = null;
|
|
114
|
+
// token address → { private, owners } from its registry record (set at deploy)
|
|
115
|
+
this._tokenRecords = new Map();
|
|
32
116
|
if (typeof this.fetchImpl !== "function") {
|
|
33
117
|
throw new Cx3Error("fetch is not available — use Node 18+ or pass options.fetch", "NO_FETCH");
|
|
34
118
|
}
|
|
@@ -38,24 +122,51 @@ class Cx3Provider {
|
|
|
38
122
|
return walletApiKeyMode(this.apiKey);
|
|
39
123
|
}
|
|
40
124
|
|
|
41
|
-
|
|
125
|
+
/** "oauth" | "apiKey" | "devNode" */
|
|
126
|
+
get authMode() {
|
|
127
|
+
if (this.devNode && !this.oauth && walletApiKeyMode(this.apiKey) === "public") return "devNode";
|
|
128
|
+
return this.oauth ? "oauth" : "apiKey";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The current access token (OAuth2 apps): fetched once, shared by concurrent calls, renewed a
|
|
133
|
+
* minute before it expires or when the gateway says it is no longer valid.
|
|
134
|
+
*/
|
|
135
|
+
async getAccessToken(force = false) {
|
|
136
|
+
if (!this.oauth) return null;
|
|
137
|
+
if (!force && this._token && Date.now() < this._token.refreshAt) return this._token.value;
|
|
138
|
+
if (!this._tokenPromise) {
|
|
139
|
+
this._tokenPromise = this._fetchToken().finally(() => { this._tokenPromise = null; });
|
|
140
|
+
}
|
|
141
|
+
return this._tokenPromise;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async _fetchToken() {
|
|
145
|
+
const form = new URLSearchParams({ grant_type: "client_credentials", client_id: this.clientId, client_secret: this._clientSecret });
|
|
146
|
+
if (this.scope) form.set("scope", this.scope);
|
|
147
|
+
const { res, json } = await this._fetchJson(`${this.baseUrl}/oauth/token`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
150
|
+
body: form.toString()
|
|
151
|
+
});
|
|
152
|
+
if (!res.ok || !json || !json.access_token) {
|
|
153
|
+
const code = (json && json.error) || "OAUTH_TOKEN_FAILED";
|
|
154
|
+
throw new Cx3HttpError((json && json.error_description) || code, res.status, { ...(json || {}), error: code });
|
|
155
|
+
}
|
|
156
|
+
const ttl = Number(json.expires_in) > 0 ? Number(json.expires_in) : 3600;
|
|
157
|
+
this._token = { value: json.access_token, refreshAt: Date.now() + Math.max(30, ttl - 60) * 1000, scope: json.scope || "" };
|
|
158
|
+
return this._token.value;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async _headers(forceToken = false) {
|
|
42
162
|
const headers = {
|
|
43
163
|
"Content-Type": "application/json",
|
|
44
164
|
...this.extraHeaders
|
|
45
165
|
};
|
|
46
|
-
if (
|
|
166
|
+
if (this.oauth) {
|
|
167
|
+
headers.Authorization = `Bearer ${await this.getAccessToken(forceToken)}`;
|
|
168
|
+
} else if (/^cx3wk_[a-f0-9]{48}$/i.test(this.apiKey)) {
|
|
47
169
|
headers["X-Api-Key"] = this.apiKey;
|
|
48
|
-
return headers;
|
|
49
|
-
}
|
|
50
|
-
if (this.auth && this.auth.user && this.auth.pass) {
|
|
51
|
-
const token = Buffer.from(`${this.auth.user}:${this.auth.pass}`).toString("base64");
|
|
52
|
-
headers.Authorization = `Basic ${token}`;
|
|
53
|
-
} else {
|
|
54
|
-
const user = process.env.CX3_INTERNAL_AUTH_USER;
|
|
55
|
-
const pass = process.env.CX3_INTERNAL_AUTH_PASS;
|
|
56
|
-
if (user && pass) {
|
|
57
|
-
headers.Authorization = `Basic ${Buffer.from(`${user}:${pass}`).toString("base64")}`;
|
|
58
|
-
}
|
|
59
170
|
}
|
|
60
171
|
return headers;
|
|
61
172
|
}
|
|
@@ -81,56 +192,218 @@ class Cx3Provider {
|
|
|
81
192
|
* @param {{ method?: string, body?: object, query?: Record<string,string|number> }} [opts]
|
|
82
193
|
*/
|
|
83
194
|
async request(path, opts = {}) {
|
|
84
|
-
if (this.apiKeyMode === "invalid") {
|
|
195
|
+
if (!this.oauth && this.apiKeyMode === "invalid") {
|
|
85
196
|
throw new Cx3Error("api_key_required", "api_key_required");
|
|
86
197
|
}
|
|
87
198
|
const method = opts.method || "GET";
|
|
88
|
-
let
|
|
89
|
-
if (opts.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
199
|
+
let route = path.startsWith("/") ? path : `/${path}`;
|
|
200
|
+
if (this.gateway) route = gatewayPath(route, opts.body);
|
|
201
|
+
const url = appendQuery(`${this.baseUrl}${this.apiPrefix}${route}`, opts.query);
|
|
202
|
+
const body = opts.body !== undefined ? JSON.stringify(opts.body) : undefined;
|
|
203
|
+
let { res, json } = await this._fetchJson(url, { method, headers: await this._headers(), body });
|
|
204
|
+
// the token expired early or the gateway restarted with a new key: one fresh token, one retry
|
|
205
|
+
if (this.oauth && res.status === 401 && json && json.error === "invalid_token") {
|
|
206
|
+
({ res, json } = await this._fetchJson(url, { method, headers: await this._headers(true), body }));
|
|
96
207
|
}
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
throw new Cx3HttpError(
|
|
210
|
+
json.error || json.message || `HTTP ${res.status}`,
|
|
211
|
+
res.status,
|
|
212
|
+
json
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return json;
|
|
216
|
+
}
|
|
97
217
|
|
|
218
|
+
/** fetch with the provider timeout; the body is parsed as JSON ({} when it is not). */
|
|
219
|
+
async _fetchJson(url, init) {
|
|
98
220
|
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
99
221
|
const timer = controller
|
|
100
222
|
? setTimeout(() => controller.abort(), this.timeoutMs)
|
|
101
223
|
: null;
|
|
102
|
-
|
|
103
224
|
try {
|
|
104
|
-
const res = await this.fetchImpl(url, {
|
|
105
|
-
method,
|
|
106
|
-
headers: this._headers(),
|
|
107
|
-
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
108
|
-
signal: controller ? controller.signal : undefined
|
|
109
|
-
});
|
|
225
|
+
const res = await this.fetchImpl(url, { ...init, signal: controller ? controller.signal : undefined });
|
|
110
226
|
const json = await res.json().catch(() => ({}));
|
|
111
|
-
|
|
112
|
-
throw new Cx3HttpError(
|
|
113
|
-
json.error || json.message || `HTTP ${res.status}`,
|
|
114
|
-
res.status,
|
|
115
|
-
json
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
return json;
|
|
227
|
+
return { res, json };
|
|
119
228
|
} finally {
|
|
120
229
|
if (timer) clearTimeout(timer);
|
|
121
230
|
}
|
|
122
231
|
}
|
|
123
232
|
|
|
233
|
+
/**
|
|
234
|
+
* GET on the gateway's public read group (`/dapp/v1{route}`: wallet tokens, activity, CarbonFi
|
|
235
|
+
* pools, WIT). These are public chain data, read without credentials; a development node has no
|
|
236
|
+
* such group (DAPP_ONLY).
|
|
237
|
+
*/
|
|
238
|
+
async _dappRead(route, query) {
|
|
239
|
+
if (!this.gateway) {
|
|
240
|
+
throw new Cx3Error(`${route} is served by the CX3Api gateway; a node does not serve it`, "DAPP_ONLY");
|
|
241
|
+
}
|
|
242
|
+
const headers = { "Content-Type": "application/json", ...this.extraHeaders };
|
|
243
|
+
const { res, json } = await this._fetchJson(appendQuery(`${this.baseUrl}/dapp/v1${route}`, query), { method: "GET", headers });
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
throw new Cx3HttpError(json.error || json.message || `HTTP ${res.status}`, res.status, json);
|
|
246
|
+
}
|
|
247
|
+
return json;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Every registry token with this wallet's balance in one read (`/dapp/v1/accounts/:address/tokens`):
|
|
252
|
+
* `{ tokens: [{ address, name, symbol, decimals, standard, balance, mine, private?, authorized? }], total, unread }`.
|
|
253
|
+
* A private token is listed only for its owner, its authorized wallets and its holders.
|
|
254
|
+
*/
|
|
255
|
+
async getTokens(address) {
|
|
256
|
+
const addr = normalizeAddress(address);
|
|
257
|
+
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
258
|
+
return this._dappRead(`/accounts/${encodeURIComponent(addr)}/tokens`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Wallet history, newest first, without block rewards (`/dapp/v1/accounts/:address/activity`).
|
|
263
|
+
* Next page: `{ before: previousPage.nextBefore }`; `nextBefore` is null on the last page.
|
|
264
|
+
*/
|
|
265
|
+
async getActivity(address, opts = {}) {
|
|
266
|
+
const addr = normalizeAddress(address);
|
|
267
|
+
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
268
|
+
return this._dappRead(`/accounts/${encodeURIComponent(addr)}/activity`, { limit: opts.limit, before: opts.before });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** CarbonFi pools (`/dapp/v1/dex/pairs`): `{ pairs }`. */
|
|
272
|
+
async getDexPairs() {
|
|
273
|
+
return this._dappRead("/dex/pairs");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** A wallet's LP token balance in one CarbonFi pool: `{ balance, totalSupply, lpContractAddress, onChain }`. */
|
|
277
|
+
async getLpBalance(pairId, address) {
|
|
278
|
+
const id = String(pairId || "").replace(/^0x/i, "");
|
|
279
|
+
if (!/^[0-9a-f]{40}$/i.test(id)) throw new Cx3Error("pair id must be 40 hex characters", "INVALID_PAIR_ID");
|
|
280
|
+
const addr = normalizeAddress(address);
|
|
281
|
+
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
282
|
+
return this._dappRead(`/dex/pairs/${id.toLowerCase()}/lp/${encodeURIComponent(addr)}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
124
285
|
async getContractInfo(address) {
|
|
125
286
|
const addr = encodeURIComponent(address);
|
|
126
287
|
return this.request(`/contract/${addr}`);
|
|
127
288
|
}
|
|
128
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Is this a private token (CX3WebUI "Private" deploy, or `Cx3TokenFactory` with
|
|
292
|
+
* `visibility: "private"`)? Read from its registry record (`metadata.visibility`), which the
|
|
293
|
+
* deploy sets for good, so the answer is kept per address.
|
|
294
|
+
*/
|
|
295
|
+
async isPrivateToken(address) {
|
|
296
|
+
return (await this._tokenRecord(address)).private;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async _tokenRecord(address) {
|
|
300
|
+
const key = normalizeAddress(address);
|
|
301
|
+
if (this._tokenRecords.has(key)) return this._tokenRecords.get(key);
|
|
302
|
+
const info = await this.getContractInfo(address);
|
|
303
|
+
const c = info && info.contract;
|
|
304
|
+
if (!c || typeof c !== "object") throw new Cx3Error("contract record not available", "CONTRACT_NOT_FOUND");
|
|
305
|
+
const rec = {
|
|
306
|
+
private: !!(c.metadata && typeof c.metadata === "object" && c.metadata.visibility === "private"),
|
|
307
|
+
owners: [c.owner, c.deployer].map(normalizeAddress).filter((a) => /^0x[0-9a-f]{40}$/.test(a))
|
|
308
|
+
};
|
|
309
|
+
this._tokenRecords.set(key, rec);
|
|
310
|
+
return rec;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* May this wallet hold and move the token? The owner always may (the templates' guards say
|
|
315
|
+
* `owner || whitelist[a]`); any other wallet when the token's `isWhitelisted` view says so (the
|
|
316
|
+
* authorized list of a private token, the whitelist of a public one). Meaningful for a token with
|
|
317
|
+
* such a list: the node answers false for a token without one.
|
|
318
|
+
* @returns {Promise<boolean|null>} null = the node did not answer
|
|
319
|
+
*/
|
|
320
|
+
async isTokenAuthorized(token, wallet) {
|
|
321
|
+
const a = normalizeAddress(wallet);
|
|
322
|
+
if (!/^0x[0-9a-f]{40}$/.test(a)) throw new Cx3Error("wallet address required", "NO_ADDRESS");
|
|
323
|
+
try {
|
|
324
|
+
if ((await this._tokenRecord(token)).owners.includes(a)) return true;
|
|
325
|
+
} catch (_) {
|
|
326
|
+
// no registry record: the view decides
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
// the templates name the parameter addr or account; the node takes the one its ABI has
|
|
330
|
+
const r = await this.call(token, "isWhitelisted", { addr: a, account: a, address: a });
|
|
331
|
+
if (!r || r.success === false) return null;
|
|
332
|
+
return boolVerdict(r.value);
|
|
333
|
+
} catch (_) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Before a signed transfer / transferFrom / mint of a private token: both ends must be the owner or
|
|
340
|
+
* authorized wallets, otherwise the block refuses it. Throws PRIVATE_TOKEN_SENDER or
|
|
341
|
+
* PRIVATE_TOKEN_RECIPIENT; an unreadable record or view gives no verdict (the contract decides).
|
|
342
|
+
*/
|
|
343
|
+
async checkPrivateTokenSend(contractAddress, method, params, from) {
|
|
344
|
+
if (!PRIVATE_GUARDED_METHODS.has(method)) return;
|
|
345
|
+
let priv = false;
|
|
346
|
+
try {
|
|
347
|
+
priv = await this.isPrivateToken(contractAddress);
|
|
348
|
+
} catch (_) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!priv) return;
|
|
352
|
+
const p = params && typeof params === "object" ? params : {};
|
|
353
|
+
const sender = method === "transferFrom" ? p.from : from;
|
|
354
|
+
const recipient = p.to != null ? p.to : p.recipient;
|
|
355
|
+
if (sender && (await this.isTokenAuthorized(contractAddress, sender)) === false) {
|
|
356
|
+
throw new Cx3Error("The sender is not an authorized wallet of this private token", "PRIVATE_TOKEN_SENDER",
|
|
357
|
+
{ token: contractAddress, wallet: normalizeAddress(sender) });
|
|
358
|
+
}
|
|
359
|
+
if (recipient && (await this.isTokenAuthorized(contractAddress, recipient)) === false) {
|
|
360
|
+
throw new Cx3Error("The recipient is not an authorized wallet of this private token", "PRIVATE_TOKEN_RECIPIENT",
|
|
361
|
+
{ token: contractAddress, wallet: normalizeAddress(recipient) });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
129
365
|
async getAbi(address) {
|
|
130
366
|
const addr = encodeURIComponent(address);
|
|
131
367
|
return this.request(`/contracts/${addr}/abi`);
|
|
132
368
|
}
|
|
133
369
|
|
|
370
|
+
/**
|
|
371
|
+
* WIT interface of a CX3WASM contract: `{ target, wit }`.
|
|
372
|
+
* On the gateway: `/dapp/v1/contracts/:address/wit` (404 WIT_NOT_AVAILABLE for CX3JS; the
|
|
373
|
+
* source itself is never served to apps); a development node: `witCode` of the source record.
|
|
374
|
+
*/
|
|
375
|
+
async getWit(address) {
|
|
376
|
+
const addr = encodeURIComponent(address);
|
|
377
|
+
if (this.gateway) return this._dappRead(`/contracts/${addr}/wit`);
|
|
378
|
+
const src = await this.request(`/contracts/${addr}/source`);
|
|
379
|
+
return { success: src.success !== false, address, target: src.target || null, wit: src.witCode || "" };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Chain head `{ height, hash, timestamp, chainId? }` (chainId from the gateway). */
|
|
383
|
+
async getStatus() {
|
|
384
|
+
if (this.gateway) return this._rejectEnvelope(await this._dappRead("/status"));
|
|
385
|
+
const blk = this._rejectEnvelope(await this.request("/getLastBlock"));
|
|
386
|
+
return { success: true, height: blk.index != null ? blk.index : blk.height, hash: blk.hash || null, timestamp: blk.timestamp || null };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* `{ chainId, genesisHash }`: the network identity a native transfer signs over (the gateway's
|
|
391
|
+
* /status, a development node's /chain/identity). Throws NO_CHAIN_IDENTITY when either is missing.
|
|
392
|
+
*/
|
|
393
|
+
async getChainIdentity() {
|
|
394
|
+
// changes only with a new genesis: kept for a minute (one request per signature otherwise)
|
|
395
|
+
const hit = this._chainIdentity;
|
|
396
|
+
if (hit && Date.now() - hit.at < 60000) return { ...hit.value };
|
|
397
|
+
const r = this.gateway
|
|
398
|
+
? this._rejectEnvelope(await this._dappRead("/status"))
|
|
399
|
+
: this._rejectEnvelope(await this.request("/chain/identity"));
|
|
400
|
+
const chainId = typeof r.chainId === "string" ? r.chainId.trim() : "";
|
|
401
|
+
const genesisHash = normalizeGenesisHash(r.genesisHash);
|
|
402
|
+
if (!chainId || !genesisHash) throw new Cx3Error("network identity unavailable", "NO_CHAIN_IDENTITY");
|
|
403
|
+
this._chainIdentity = { at: Date.now(), value: { chainId, genesisHash } };
|
|
404
|
+
return { chainId, genesisHash };
|
|
405
|
+
}
|
|
406
|
+
|
|
134
407
|
async getEvents(address, typeOrOpts = "", opts = {}) {
|
|
135
408
|
const extra = (typeOrOpts && typeof typeOrOpts === "object")
|
|
136
409
|
? typeOrOpts
|
|
@@ -223,21 +496,23 @@ class Cx3Provider {
|
|
|
223
496
|
}
|
|
224
497
|
|
|
225
498
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
499
|
+
* Check that this wallet is the one the credentials belong to (an app and a wallet API key are
|
|
500
|
+
* both bound to one wallet): `{ ok: true }`, or `wallet_mismatch`.
|
|
501
|
+
* OAuth2 app → GET `/wallet/api-key/bind`; cx3wk_ key → GET `/whoami`, then `/wallet/api-key/bind` on 404.
|
|
502
|
+
* A development node has no credentials: `{ ok: true, devNode: true }`.
|
|
229
503
|
*/
|
|
230
504
|
async bindApiKey(address) {
|
|
231
505
|
const addr = normalizeAddress(address);
|
|
232
506
|
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
507
|
+
const query = { address: addr };
|
|
508
|
+
if (this.oauth) return this._rejectEnvelope(await this.request("/wallet/api-key/bind", { query }));
|
|
233
509
|
const mode = this.apiKeyMode;
|
|
234
510
|
if (mode === "invalid") {
|
|
235
511
|
throw new Cx3Error("api_key_required", "api_key_required");
|
|
236
512
|
}
|
|
237
513
|
if (mode === "public") {
|
|
238
|
-
return { ok: true,
|
|
514
|
+
return { ok: true, devNode: true };
|
|
239
515
|
}
|
|
240
|
-
const query = { address: addr };
|
|
241
516
|
try {
|
|
242
517
|
const json = await this.request("/whoami", { query });
|
|
243
518
|
return this._rejectEnvelope(json);
|
|
@@ -250,8 +525,18 @@ class Cx3Provider {
|
|
|
250
525
|
}
|
|
251
526
|
}
|
|
252
527
|
|
|
253
|
-
|
|
254
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Registry list. The gateway shows a private token only when the app's wallet is its owner or an
|
|
530
|
+
* authorized wallet; `owner`, `standard`, `type` filter and `live` adds supply / paused.
|
|
531
|
+
*/
|
|
532
|
+
async listContracts({ limit = 50, offset = 0, viewer, owner, standard, type, live } = {}) {
|
|
533
|
+
const query = { limit, offset };
|
|
534
|
+
if (viewer) query.viewer = normalizeAddress(viewer);
|
|
535
|
+
if (owner) query.owner = normalizeAddress(owner);
|
|
536
|
+
if (standard) query.standard = standard;
|
|
537
|
+
if (type) query.type = type;
|
|
538
|
+
if (live) query.live = 1;
|
|
539
|
+
return this.request("/getContracts", { query });
|
|
255
540
|
}
|
|
256
541
|
|
|
257
542
|
/**
|
|
@@ -261,7 +546,7 @@ class Cx3Provider {
|
|
|
261
546
|
const body = {
|
|
262
547
|
contractAddress,
|
|
263
548
|
method,
|
|
264
|
-
params: params || {}
|
|
549
|
+
params: toJsonSafe(params || {})
|
|
265
550
|
};
|
|
266
551
|
if (sender) body.fromAddress = normalizeAddress(sender);
|
|
267
552
|
const json = await this.request("/contract/call", { method: "POST", body });
|
|
@@ -279,7 +564,7 @@ class Cx3Provider {
|
|
|
279
564
|
const body = {
|
|
280
565
|
contractAddress,
|
|
281
566
|
method,
|
|
282
|
-
params: params || {},
|
|
567
|
+
params: toJsonSafe(params || {}),
|
|
283
568
|
fromAddress: normalizeAddress(extras.fromAddress || extras.sender || ""),
|
|
284
569
|
...extras
|
|
285
570
|
};
|
|
@@ -297,9 +582,9 @@ class Cx3Provider {
|
|
|
297
582
|
try {
|
|
298
583
|
return await this.request(`/tx/${hash}/status`);
|
|
299
584
|
} catch (e) {
|
|
300
|
-
if (e.status === 404) {
|
|
585
|
+
if (e.status === 404 && !this.gateway) {
|
|
301
586
|
const url = `${this.baseUrl}/tx/${hash}/status`;
|
|
302
|
-
const res = await this.fetchImpl(url, { headers: this._headers() });
|
|
587
|
+
const res = await this.fetchImpl(url, { headers: await this._headers() });
|
|
303
588
|
const json = await res.json().catch(() => ({}));
|
|
304
589
|
if (!res.ok) throw new Cx3HttpError(json.error || `HTTP ${res.status}`, res.status, json);
|
|
305
590
|
return json;
|
package/src/utils.js
CHANGED
|
@@ -48,6 +48,24 @@ function isPlainObject(v) {
|
|
|
48
48
|
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* BigInt → decimal string, recursively, so uint256 amounts can be passed as
|
|
53
|
+
* BigInt. Runs BEFORE signing: the node rebuilds the digest from the JSON it
|
|
54
|
+
* receives, so the signed params and the sent params must be the same values.
|
|
55
|
+
*/
|
|
56
|
+
function toJsonSafe(value) {
|
|
57
|
+
if (typeof value === "bigint") return value.toString();
|
|
58
|
+
if (Array.isArray(value)) return value.map(toJsonSafe);
|
|
59
|
+
// only literal objects: Date / Buffer keep their own toJSON
|
|
60
|
+
const proto = value !== null && typeof value === "object" ? Object.getPrototypeOf(value) : undefined;
|
|
61
|
+
if (proto === Object.prototype || proto === null) {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [k, v] of Object.entries(value)) out[k] = toJsonSafe(v);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
/**
|
|
52
70
|
* Same rules as AppSmokeTest `cx3_dapp_key_mode`:
|
|
53
71
|
* empty → public (any wallet); valid cx3wk_ → private (that wallet); else invalid.
|
|
@@ -66,5 +84,6 @@ module.exports = {
|
|
|
66
84
|
normalizeAddress,
|
|
67
85
|
sleep,
|
|
68
86
|
isPlainObject,
|
|
87
|
+
toJsonSafe,
|
|
69
88
|
walletApiKeyMode
|
|
70
89
|
};
|