@carbonx3/cx3.js 0.1.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 +138 -0
- package/index.d.ts +157 -0
- package/index.js +67 -0
- package/package.json +45 -0
- package/src/abi.js +117 -0
- package/src/contract.js +113 -0
- package/src/crypto.js +90 -0
- package/src/digest.js +80 -0
- package/src/errors.js +29 -0
- package/src/factory.js +130 -0
- package/src/provider.js +361 -0
- package/src/transaction.js +53 -0
- package/src/utils.js +70 -0
- package/src/wallet.js +247 -0
package/src/crypto.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const secp = require("@noble/secp256k1");
|
|
5
|
+
const { strip0x, ensure0x } = require("./utils");
|
|
6
|
+
const { Cx3Error } = require("./errors");
|
|
7
|
+
|
|
8
|
+
function sha256(data) {
|
|
9
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
10
|
+
return crypto.createHash("sha256").update(buf).digest();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function privateKeyToBytes(input) {
|
|
14
|
+
if (input instanceof Uint8Array || Buffer.isBuffer(input)) {
|
|
15
|
+
if (input.length !== 32) throw new Cx3Error("Invalid private key length", "INVALID_PRIVATE_KEY");
|
|
16
|
+
return Buffer.from(input);
|
|
17
|
+
}
|
|
18
|
+
const hex = strip0x(String(input || "")).toLowerCase();
|
|
19
|
+
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
20
|
+
throw new Cx3Error("Invalid private key hex", "INVALID_PRIVATE_KEY");
|
|
21
|
+
}
|
|
22
|
+
return Buffer.from(hex, "hex");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function privateKeyToPublicKey(privateKeyInput, compressed = true) {
|
|
26
|
+
const priv = privateKeyToBytes(privateKeyInput);
|
|
27
|
+
const pub = secp.getPublicKey(priv, compressed);
|
|
28
|
+
return Buffer.from(pub);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Address derivation matches CX3Wallet / Green Chain:
|
|
33
|
+
* address = 0x + last 20 bytes of sha256(uncompressed_pubkey)
|
|
34
|
+
*/
|
|
35
|
+
function privateKeyToAddress(privateKeyInput) {
|
|
36
|
+
const pubUncompressed = privateKeyToPublicKey(privateKeyInput, false);
|
|
37
|
+
const hash = sha256(pubUncompressed);
|
|
38
|
+
return ensure0x(hash.slice(-20).toString("hex"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sign a UTF-8 digest string (or bytes).
|
|
43
|
+
* Returns compact r||s hex (64 bytes) + compressed public key — same shape as CX3Wallet.
|
|
44
|
+
*/
|
|
45
|
+
async function signMessage(message, privateKeyInput) {
|
|
46
|
+
const priv = privateKeyToBytes(privateKeyInput);
|
|
47
|
+
const msgBytes = typeof message === "string"
|
|
48
|
+
? Buffer.from(message, "utf8")
|
|
49
|
+
: Buffer.from(message);
|
|
50
|
+
const msgHash = sha256(msgBytes);
|
|
51
|
+
const signed = await secp.sign(msgHash, priv, { der: false, recovered: true });
|
|
52
|
+
const sigBytes = Array.isArray(signed) ? signed[0] : signed;
|
|
53
|
+
const recovery = Array.isArray(signed) ? signed[1] : (signed.recovery != null ? signed.recovery : 0);
|
|
54
|
+
const signature = Buffer.from(sigBytes).toString("hex");
|
|
55
|
+
const publicKey = privateKeyToPublicKey(priv, true).toString("hex");
|
|
56
|
+
const publicKeyUncompressed = privateKeyToPublicKey(priv, false).toString("hex");
|
|
57
|
+
return {
|
|
58
|
+
signature,
|
|
59
|
+
r: signature,
|
|
60
|
+
s: null,
|
|
61
|
+
v: (recovery != null ? recovery : 0) + 27,
|
|
62
|
+
publicKey,
|
|
63
|
+
senderPublicKey: publicKey,
|
|
64
|
+
publicKeyUncompressed,
|
|
65
|
+
hash: ensure0x(msgHash.toString("hex")),
|
|
66
|
+
derivedAddress: privateKeyToAddress(priv)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createPrivateKey() {
|
|
71
|
+
for (let i = 0; i < 64; i++) {
|
|
72
|
+
const priv = crypto.randomBytes(32);
|
|
73
|
+
try {
|
|
74
|
+
secp.getPublicKey(priv, false);
|
|
75
|
+
return priv.toString("hex");
|
|
76
|
+
} catch (_) {
|
|
77
|
+
/* retry */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw new Cx3Error("Could not generate private key", "KEYGEN_FAILED");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
sha256,
|
|
85
|
+
privateKeyToBytes,
|
|
86
|
+
privateKeyToPublicKey,
|
|
87
|
+
privateKeyToAddress,
|
|
88
|
+
signMessage,
|
|
89
|
+
createPrivateKey
|
|
90
|
+
};
|
package/src/digest.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical digests must stay byte-identical to
|
|
5
|
+
* CX3GreenChain/src/api/controllers/ContractController.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function buildDeployDigest({
|
|
9
|
+
owner,
|
|
10
|
+
standard,
|
|
11
|
+
name,
|
|
12
|
+
symbol,
|
|
13
|
+
decimals = 18,
|
|
14
|
+
initialSupply = "0",
|
|
15
|
+
timestamp
|
|
16
|
+
}) {
|
|
17
|
+
return [
|
|
18
|
+
"deploy",
|
|
19
|
+
owner || "",
|
|
20
|
+
standard || "",
|
|
21
|
+
name || "",
|
|
22
|
+
symbol || "",
|
|
23
|
+
decimals !== undefined ? decimals : 18,
|
|
24
|
+
initialSupply !== undefined ? String(initialSupply) : "0",
|
|
25
|
+
timestamp || 0
|
|
26
|
+
].join("|");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildSendDigest(contractAddress, method, params, fromAddress, timestamp) {
|
|
30
|
+
const base = `${contractAddress}|${method}|${JSON.stringify(params || {})}|${fromAddress || ""}`;
|
|
31
|
+
if (timestamp !== undefined && timestamp !== null && timestamp !== "") {
|
|
32
|
+
return `${base}|${timestamp}`;
|
|
33
|
+
}
|
|
34
|
+
return base;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildOwnerActionDigest(action, address, owner, timestamp) {
|
|
38
|
+
return `${action}|${address}|${owner}|${timestamp}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stripNativeAddress(addr) {
|
|
42
|
+
const s = String(addr || "").trim();
|
|
43
|
+
if (!s) return "";
|
|
44
|
+
if (s.startsWith("cx3:")) return s.slice(4).toLowerCase();
|
|
45
|
+
if (s.startsWith("0x") || s.startsWith("0X")) return s.slice(2).toLowerCase();
|
|
46
|
+
return s.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Canonical native TRANSFER payload — byte-identical to
|
|
51
|
+
* CX3GreenChain Transaction.canonicalPayload / CX3Wallet CX3Tx.buildTransferPayload.
|
|
52
|
+
*/
|
|
53
|
+
function buildTransferPayload(fields = {}) {
|
|
54
|
+
const payload = {
|
|
55
|
+
type: fields.type || "TRANSFER",
|
|
56
|
+
fromAddress: stripNativeAddress(fields.fromAddress),
|
|
57
|
+
toAddress: stripNativeAddress(fields.toAddress),
|
|
58
|
+
amount: fields.amount ?? 0,
|
|
59
|
+
nonce: fields.nonce,
|
|
60
|
+
timestamp: fields.timestamp,
|
|
61
|
+
data: fields.data || null,
|
|
62
|
+
gasLimit: typeof fields.gasLimit === "number" && fields.gasLimit > 0 ? fields.gasLimit : 0,
|
|
63
|
+
gasPrice: typeof fields.gasPrice === "number" && fields.gasPrice > 0 ? fields.gasPrice : 0
|
|
64
|
+
};
|
|
65
|
+
const bind = fields.chainIdBind != null
|
|
66
|
+
? !!fields.chainIdBind
|
|
67
|
+
: process.env.CX3_CHAIN_ID_BIND !== "0";
|
|
68
|
+
if (bind) {
|
|
69
|
+
payload.chainId = String(fields.chainId || process.env.CX3_CHAIN_ID || "cx3-dev");
|
|
70
|
+
}
|
|
71
|
+
return JSON.stringify(payload);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
buildDeployDigest,
|
|
76
|
+
buildSendDigest,
|
|
77
|
+
buildOwnerActionDigest,
|
|
78
|
+
buildTransferPayload,
|
|
79
|
+
stripNativeAddress
|
|
80
|
+
};
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
class Cx3Error extends Error {
|
|
4
|
+
constructor(message, code = "CX3_ERROR", details = null) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "Cx3Error";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class Cx3HttpError extends Cx3Error {
|
|
13
|
+
constructor(message, status, body = null) {
|
|
14
|
+
super(message, (body && (body.error || body.code)) || `HTTP_${status}`, body);
|
|
15
|
+
this.name = "Cx3HttpError";
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.body = body;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class Cx3TxError extends Cx3Error {
|
|
22
|
+
constructor(message, code = "TX_FAILED", receipt = null) {
|
|
23
|
+
super(message, code, receipt);
|
|
24
|
+
this.name = "Cx3TxError";
|
|
25
|
+
this.receipt = receipt;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { Cx3Error, Cx3HttpError, Cx3TxError };
|
package/src/factory.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { Cx3Error } = require("./errors");
|
|
4
|
+
const { Contract } = require("./contract");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Standard token deploy helpers.
|
|
8
|
+
*/
|
|
9
|
+
class Cx3TokenFactory {
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('./wallet').Cx3Wallet} wallet Must have a provider
|
|
12
|
+
*/
|
|
13
|
+
constructor(wallet) {
|
|
14
|
+
if (!wallet || !wallet.deployContract) {
|
|
15
|
+
throw new Cx3Error("Cx3TokenFactory requires a Cx3Wallet", "NO_WALLET");
|
|
16
|
+
}
|
|
17
|
+
this.wallet = wallet;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Deploy CX30 fungible token.
|
|
22
|
+
* @param {object} fields name, symbol, decimals, initialSupply, capabilities, target, …
|
|
23
|
+
* @returns {Promise<{ contractAddress, txHash, blockHeight, contract?: Contract }>}
|
|
24
|
+
*/
|
|
25
|
+
async deployCX30(fields = {}) {
|
|
26
|
+
return this._deployStandard("CX30", {
|
|
27
|
+
decimals: 18,
|
|
28
|
+
initialSupply: "0",
|
|
29
|
+
mintable: true,
|
|
30
|
+
burnable: true,
|
|
31
|
+
pausable: true,
|
|
32
|
+
target: "CX3WASM",
|
|
33
|
+
...fields
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async deployCX31(fields = {}) {
|
|
38
|
+
return this._deployStandard("CX31", { decimals: 18, target: "CX3WASM", ...fields });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async deployCX300(fields = {}) {
|
|
42
|
+
return this._deployStandard("CX300", { decimals: 18, target: "CX3WASM", ...fields });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async deployCX315(fields = {}) {
|
|
46
|
+
return this._deployStandard("CX315", { decimals: 0, target: "CX3WASM", ...fields });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async deployCX3192(fields = {}) {
|
|
50
|
+
return this._deployStandard("CX3192", { decimals: 0, target: "CX3WASM", ...fields });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async deployCX337(fields = {}) {
|
|
54
|
+
return this._deployStandard("CX337", {
|
|
55
|
+
name: "CX337 CBAM Proof",
|
|
56
|
+
symbol: "CX337CBAM",
|
|
57
|
+
decimals: 0,
|
|
58
|
+
target: "CX3WASM",
|
|
59
|
+
...fields
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async deployCX338(fields = {}) {
|
|
64
|
+
return this._deployStandard("CX338", {
|
|
65
|
+
name: "CX338 Sustainability Report",
|
|
66
|
+
symbol: "CX338SUS",
|
|
67
|
+
decimals: 0,
|
|
68
|
+
target: "CX3WASM",
|
|
69
|
+
...fields
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async deployCX339(fields = {}) {
|
|
74
|
+
return this._deployStandard("CX339", {
|
|
75
|
+
name: "CX339 Supply Chain",
|
|
76
|
+
symbol: "CX339SC",
|
|
77
|
+
decimals: 0,
|
|
78
|
+
target: "CX3WASM",
|
|
79
|
+
...fields
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async deployCX340(fields = {}) {
|
|
84
|
+
return this._deployStandard("CX340", {
|
|
85
|
+
name: "CX340 Materiality Assessment",
|
|
86
|
+
symbol: "CX340MAT",
|
|
87
|
+
decimals: 0,
|
|
88
|
+
target: "CX3WASM",
|
|
89
|
+
...fields
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async deployCX341(fields = {}) {
|
|
94
|
+
return this._deployStandard("CX341", {
|
|
95
|
+
name: "CX341 Green Claims",
|
|
96
|
+
symbol: "CX341GC",
|
|
97
|
+
decimals: 0,
|
|
98
|
+
target: "CX3WASM",
|
|
99
|
+
...fields
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async _deployStandard(standard, fields) {
|
|
104
|
+
if (!fields.name || !fields.symbol) {
|
|
105
|
+
throw new Cx3Error("name and symbol are required", "BAD_DEPLOY_ARGS");
|
|
106
|
+
}
|
|
107
|
+
const result = await this.wallet.deployContract({
|
|
108
|
+
...fields,
|
|
109
|
+
standard
|
|
110
|
+
});
|
|
111
|
+
if (result && result.success !== false && result.contractAddress) {
|
|
112
|
+
try {
|
|
113
|
+
result.contract = await this.wallet.provider.getContract(
|
|
114
|
+
result.contractAddress,
|
|
115
|
+
null,
|
|
116
|
+
this.wallet
|
|
117
|
+
);
|
|
118
|
+
} catch (_) {
|
|
119
|
+
result.contract = new Contract(
|
|
120
|
+
result.contractAddress,
|
|
121
|
+
{ methods: {} },
|
|
122
|
+
this.wallet
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { Cx3TokenFactory };
|
package/src/provider.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { Cx3Error, Cx3HttpError, Cx3TxError } = require("./errors");
|
|
4
|
+
const { readCallValue, sleep, normalizeAddress, walletApiKeyMode } = require("./utils");
|
|
5
|
+
const { Contract } = require("./contract");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Green Chain HTTP provider.
|
|
9
|
+
*
|
|
10
|
+
* Talks to `{baseUrl}/api/v1/...` — never CX3VM directly.
|
|
11
|
+
*/
|
|
12
|
+
class Cx3Provider {
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} baseUrl Green Chain node URL (`CX3_NODE_URL`)
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {string} [options.apiPrefix="/api/v1"]
|
|
17
|
+
* @param {{ user: string, pass: string }} [options.auth] Basic auth (legacy node → Green Chain)
|
|
18
|
+
* @param {string} [options.apiKey] Wallet API key (`cx3wk_…`) for CX3Api `X-Api-Key`
|
|
19
|
+
* @param {Record<string,string>} [options.headers] Extra headers
|
|
20
|
+
* @param {typeof fetch} [options.fetch] Custom fetch (tests)
|
|
21
|
+
* @param {number} [options.timeoutMs]
|
|
22
|
+
*/
|
|
23
|
+
constructor(baseUrl, options = {}) {
|
|
24
|
+
if (!baseUrl) throw new Cx3Error("Cx3Provider requires baseUrl (CX3_NODE_URL)", "NO_BASE_URL");
|
|
25
|
+
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
26
|
+
this.apiPrefix = options.apiPrefix != null ? options.apiPrefix : "/api/v1";
|
|
27
|
+
this.auth = options.auth || null;
|
|
28
|
+
this.apiKey = String(options.apiKey != null ? options.apiKey : (process.env.CX3_API_KEY || "")).trim();
|
|
29
|
+
this.extraHeaders = options.headers || {};
|
|
30
|
+
this.fetchImpl = options.fetch || globalThis.fetch;
|
|
31
|
+
this.timeoutMs = options.timeoutMs || 60000;
|
|
32
|
+
if (typeof this.fetchImpl !== "function") {
|
|
33
|
+
throw new Cx3Error("fetch is not available — use Node 18+ or pass options.fetch", "NO_FETCH");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get apiKeyMode() {
|
|
38
|
+
return walletApiKeyMode(this.apiKey);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_headers() {
|
|
42
|
+
const headers = {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
...this.extraHeaders
|
|
45
|
+
};
|
|
46
|
+
if (/^cx3wk_[a-f0-9]{48}$/i.test(this.apiKey)) {
|
|
47
|
+
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
|
+
}
|
|
60
|
+
return headers;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_rejectEnvelope(json) {
|
|
64
|
+
if (json && json.error) {
|
|
65
|
+
throw new Cx3Error(String(json.error), String(json.error), json);
|
|
66
|
+
}
|
|
67
|
+
return json;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_pageQuery(opts = {}) {
|
|
71
|
+
const query = {};
|
|
72
|
+
if (opts.type != null && opts.type !== "") query.type = opts.type;
|
|
73
|
+
if (opts.limit != null && opts.limit !== "") query.limit = opts.limit;
|
|
74
|
+
if (opts.offset != null && opts.offset !== "") query.offset = opts.offset;
|
|
75
|
+
return query;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Low-level HTTP helper.
|
|
80
|
+
* @param {string} path e.g. `/contract/call`
|
|
81
|
+
* @param {{ method?: string, body?: object, query?: Record<string,string|number> }} [opts]
|
|
82
|
+
*/
|
|
83
|
+
async request(path, opts = {}) {
|
|
84
|
+
if (this.apiKeyMode === "invalid") {
|
|
85
|
+
throw new Cx3Error("api_key_required", "api_key_required");
|
|
86
|
+
}
|
|
87
|
+
const method = opts.method || "GET";
|
|
88
|
+
let url = `${this.baseUrl}${this.apiPrefix}${path.startsWith("/") ? path : `/${path}`}`;
|
|
89
|
+
if (opts.query) {
|
|
90
|
+
const qs = new URLSearchParams();
|
|
91
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
92
|
+
if (v !== undefined && v !== null && v !== "") qs.set(k, String(v));
|
|
93
|
+
}
|
|
94
|
+
const s = qs.toString();
|
|
95
|
+
if (s) url += (url.includes("?") ? "&" : "?") + s;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
99
|
+
const timer = controller
|
|
100
|
+
? setTimeout(() => controller.abort(), this.timeoutMs)
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
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
|
+
});
|
|
110
|
+
const json = await res.json().catch(() => ({}));
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
throw new Cx3HttpError(
|
|
113
|
+
json.error || json.message || `HTTP ${res.status}`,
|
|
114
|
+
res.status,
|
|
115
|
+
json
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return json;
|
|
119
|
+
} finally {
|
|
120
|
+
if (timer) clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getContractInfo(address) {
|
|
125
|
+
const addr = encodeURIComponent(address);
|
|
126
|
+
return this.request(`/contract/${addr}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getAbi(address) {
|
|
130
|
+
const addr = encodeURIComponent(address);
|
|
131
|
+
return this.request(`/contracts/${addr}/abi`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getEvents(address, typeOrOpts = "", opts = {}) {
|
|
135
|
+
const extra = (typeOrOpts && typeof typeOrOpts === "object")
|
|
136
|
+
? typeOrOpts
|
|
137
|
+
: { type: typeOrOpts, limit: opts.limit, offset: opts.offset };
|
|
138
|
+
const addr = encodeURIComponent(address);
|
|
139
|
+
return this.request(`/contracts/${addr}/events`, {
|
|
140
|
+
query: this._pageQuery({
|
|
141
|
+
type: extra.type,
|
|
142
|
+
limit: extra.limit,
|
|
143
|
+
offset: extra.offset
|
|
144
|
+
})
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getHolders(address, opts = {}) {
|
|
149
|
+
const extra = (opts && typeof opts === "object") ? opts : {};
|
|
150
|
+
const addr = encodeURIComponent(address);
|
|
151
|
+
return this.request(`/contracts/${addr}/holders`, {
|
|
152
|
+
query: this._pageQuery({ limit: extra.limit, offset: extra.offset })
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async getTransfers(address, limitOrOpts = 20, opts = {}) {
|
|
157
|
+
let limit = 20;
|
|
158
|
+
let offset;
|
|
159
|
+
if (limitOrOpts && typeof limitOrOpts === "object") {
|
|
160
|
+
limit = limitOrOpts.limit != null ? limitOrOpts.limit : 20;
|
|
161
|
+
offset = limitOrOpts.offset;
|
|
162
|
+
} else {
|
|
163
|
+
limit = limitOrOpts;
|
|
164
|
+
offset = opts && opts.offset;
|
|
165
|
+
}
|
|
166
|
+
const addr = encodeURIComponent(address);
|
|
167
|
+
return this.request(`/contracts/${addr}/transfers`, {
|
|
168
|
+
query: this._pageQuery({ limit, offset })
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getTransferByHash(address, hash) {
|
|
173
|
+
const addr = encodeURIComponent(address);
|
|
174
|
+
const tx = encodeURIComponent(hash);
|
|
175
|
+
return this.request(`/contracts/${addr}/transfers/${tx}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async getContractState(address) {
|
|
179
|
+
const addr = encodeURIComponent(address);
|
|
180
|
+
return this.request(`/contracts/${addr}/state`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getContractSource(address) {
|
|
184
|
+
const addr = encodeURIComponent(address);
|
|
185
|
+
return this.request(`/contracts/${addr}/source`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getContractBytecode(address) {
|
|
189
|
+
const addr = encodeURIComponent(address);
|
|
190
|
+
return this.request(`/contracts/${addr}/bytecode`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async getAddressTransactions(address, limit = 20) {
|
|
194
|
+
const addr = encodeURIComponent(address);
|
|
195
|
+
return this.request(`/address/${addr}/txs`, { query: { limit } });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async getBalanceAndNonce(address) {
|
|
199
|
+
const addr = normalizeAddress(address);
|
|
200
|
+
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
201
|
+
const json = await this.request("/getBalanceAndNonce", { query: { address: addr } });
|
|
202
|
+
return this._rejectEnvelope(json);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getBalance(address) {
|
|
206
|
+
const json = await this.getBalanceAndNonce(address);
|
|
207
|
+
return json.balance;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async getNonce(address) {
|
|
211
|
+
const addr = normalizeAddress(address);
|
|
212
|
+
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
213
|
+
const json = await this.request("/getNonce", { query: { address: addr } });
|
|
214
|
+
this._rejectEnvelope(json);
|
|
215
|
+
return json.nonce;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getPendingNonce(address) {
|
|
219
|
+
const addr = normalizeAddress(address);
|
|
220
|
+
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
221
|
+
const json = await this.request("/getPendingNonce", { query: { address: addr } });
|
|
222
|
+
return this._rejectEnvelope(json);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Bind / verify the presented cx3wk_ API key against a wallet address.
|
|
227
|
+
* Empty key → public (any wallet), `{ ok: true, public: true }` — same as dApp whoami.
|
|
228
|
+
* Valid cx3wk_ → private: GET `/whoami` then CX3Api `/wallet/api-key/bind` on 404.
|
|
229
|
+
*/
|
|
230
|
+
async bindApiKey(address) {
|
|
231
|
+
const addr = normalizeAddress(address);
|
|
232
|
+
if (!addr) throw new Cx3Error("address required", "NO_ADDRESS");
|
|
233
|
+
const mode = this.apiKeyMode;
|
|
234
|
+
if (mode === "invalid") {
|
|
235
|
+
throw new Cx3Error("api_key_required", "api_key_required");
|
|
236
|
+
}
|
|
237
|
+
if (mode === "public") {
|
|
238
|
+
return { ok: true, public: true };
|
|
239
|
+
}
|
|
240
|
+
const query = { address: addr };
|
|
241
|
+
try {
|
|
242
|
+
const json = await this.request("/whoami", { query });
|
|
243
|
+
return this._rejectEnvelope(json);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
if (e && e.status === 404) {
|
|
246
|
+
const json = await this.request("/wallet/api-key/bind", { query });
|
|
247
|
+
return this._rejectEnvelope(json);
|
|
248
|
+
}
|
|
249
|
+
throw e;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async listContracts({ limit = 50, offset = 0 } = {}) {
|
|
254
|
+
return this.request("/getContracts", { query: { limit, offset } });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Read-only contract call.
|
|
259
|
+
*/
|
|
260
|
+
async call(contractAddress, method, params = {}, sender = "") {
|
|
261
|
+
const body = {
|
|
262
|
+
contractAddress,
|
|
263
|
+
method,
|
|
264
|
+
params: params || {}
|
|
265
|
+
};
|
|
266
|
+
if (sender) body.fromAddress = normalizeAddress(sender);
|
|
267
|
+
const json = await this.request("/contract/call", { method: "POST", body });
|
|
268
|
+
return {
|
|
269
|
+
...json,
|
|
270
|
+
value: readCallValue(json)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* State-changing call (unsigned unless signature fields are set).
|
|
276
|
+
* Prefer Cx3Wallet / Contract for signed sends.
|
|
277
|
+
*/
|
|
278
|
+
async send(contractAddress, method, params = {}, extras = {}) {
|
|
279
|
+
const body = {
|
|
280
|
+
contractAddress,
|
|
281
|
+
method,
|
|
282
|
+
params: params || {},
|
|
283
|
+
fromAddress: normalizeAddress(extras.fromAddress || extras.sender || ""),
|
|
284
|
+
...extras
|
|
285
|
+
};
|
|
286
|
+
delete body.sender;
|
|
287
|
+
return this.request("/contract/send", {
|
|
288
|
+
method: "POST",
|
|
289
|
+
body,
|
|
290
|
+
query: extras.wait === 0 ? { wait: 0 } : undefined
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async getTxStatus(txHash) {
|
|
295
|
+
const hash = encodeURIComponent(txHash);
|
|
296
|
+
// Prefer /api/v1 if available; fall back to gateway-style /tx/... under base.
|
|
297
|
+
try {
|
|
298
|
+
return await this.request(`/tx/${hash}/status`);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
if (e.status === 404) {
|
|
301
|
+
const url = `${this.baseUrl}/tx/${hash}/status`;
|
|
302
|
+
const res = await this.fetchImpl(url, { headers: this._headers() });
|
|
303
|
+
const json = await res.json().catch(() => ({}));
|
|
304
|
+
if (!res.ok) throw new Cx3HttpError(json.error || `HTTP ${res.status}`, res.status, json);
|
|
305
|
+
return json;
|
|
306
|
+
}
|
|
307
|
+
throw e;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Poll until confirmed / failed / timeout.
|
|
313
|
+
* @returns {Promise<object>} terminal receipt
|
|
314
|
+
*/
|
|
315
|
+
async waitForTransaction(txHash, opts = {}) {
|
|
316
|
+
const timeoutMs = opts.timeoutMs || 60000;
|
|
317
|
+
let interval = opts.intervalMs || 1000;
|
|
318
|
+
const start = Date.now();
|
|
319
|
+
while (Date.now() - start < timeoutMs) {
|
|
320
|
+
await sleep(interval);
|
|
321
|
+
const status = await this.getTxStatus(txHash);
|
|
322
|
+
if (typeof opts.onPending === "function" && status.status === "pending") {
|
|
323
|
+
try { opts.onPending(status); } catch (_) { /* ignore */ }
|
|
324
|
+
}
|
|
325
|
+
if (status.status === "confirmed") {
|
|
326
|
+
if (typeof opts.onConfirmed === "function") {
|
|
327
|
+
try { opts.onConfirmed(status); } catch (_) { /* ignore */ }
|
|
328
|
+
}
|
|
329
|
+
return status;
|
|
330
|
+
}
|
|
331
|
+
if (status.status === "failed") {
|
|
332
|
+
throw new Cx3TxError(status.error || "TX_FAILED", "TX_FAILED", status);
|
|
333
|
+
}
|
|
334
|
+
interval = Math.min(5000, Math.round(interval * 1.5));
|
|
335
|
+
}
|
|
336
|
+
const err = new Cx3TxError("TX_TRACK_TIMEOUT", "TX_TRACK_TIMEOUT", { txHash });
|
|
337
|
+
err.txHash = txHash;
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Load ABI from chain (optional override) and return a Contract handle.
|
|
343
|
+
* @param {string} address
|
|
344
|
+
* @param {object|array|null} [abi]
|
|
345
|
+
* @param {import('./wallet').Cx3Wallet|null} [signer]
|
|
346
|
+
*/
|
|
347
|
+
async getContract(address, abi = null, signer = null) {
|
|
348
|
+
let resolvedAbi = abi;
|
|
349
|
+
let target = null;
|
|
350
|
+
if (!resolvedAbi) {
|
|
351
|
+
const meta = await this.getAbi(address);
|
|
352
|
+
resolvedAbi = meta.abi || meta;
|
|
353
|
+
target = meta.target || null;
|
|
354
|
+
}
|
|
355
|
+
const c = new Contract(address, resolvedAbi, signer || this);
|
|
356
|
+
if (target) c.target = target;
|
|
357
|
+
return c;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = { Cx3Provider };
|