@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.
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ const { Cx3TxError } = require("./errors");
4
+
5
+ /**
6
+ * Result of a mutating send / deploy.
7
+ */
8
+ class TransactionResponse {
9
+ /**
10
+ * @param {object} result Raw Green Chain JSON
11
+ * @param {import('./provider').Cx3Provider|null} provider
12
+ * @param {object} [opts]
13
+ */
14
+ constructor(result, provider = null, opts = {}) {
15
+ this.raw = result || {};
16
+ this.provider = provider;
17
+ this.success = this.raw.success !== false;
18
+ this.txHash = this.raw.txHash || this.raw.hash || null;
19
+ this.hash = this.txHash;
20
+ this.blockHeight = this.raw.blockHeight;
21
+ this.contractAddress = this.raw.contractAddress || null;
22
+ this.async = !!this.raw.async || opts.wait === 0;
23
+ this.status = this.raw.status || (this.async ? "pending" : (this.success ? "confirmed" : "failed"));
24
+ this.error = this.raw.error || this.raw.message || null;
25
+ }
26
+
27
+ /**
28
+ * Wait for confirmation when the node returned async / pending.
29
+ * Sync responses that already include blockHeight resolve immediately.
30
+ */
31
+ async wait(opts = {}) {
32
+ if (!this.success) {
33
+ throw new Cx3TxError(this.error || "TX_FAILED", this.error || "TX_FAILED", this.raw);
34
+ }
35
+ if (!this.async && this.blockHeight != null) {
36
+ return {
37
+ status: "confirmed",
38
+ txHash: this.txHash,
39
+ blockHeight: this.blockHeight,
40
+ ...this.raw
41
+ };
42
+ }
43
+ if (!this.txHash) {
44
+ return this.raw;
45
+ }
46
+ if (!this.provider) {
47
+ throw new Cx3TxError("No provider to wait on transaction", "NO_PROVIDER", this.raw);
48
+ }
49
+ return this.provider.waitForTransaction(this.txHash, opts);
50
+ }
51
+ }
52
+
53
+ module.exports = { TransactionResponse };
package/src/utils.js ADDED
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Normalize Green Chain read responses:
5
+ * metadata fast-path uses `result`, VM path uses `return`,
6
+ * some proxies (and the browser IIFE) expose `value`.
7
+ */
8
+ function readCallValue(response) {
9
+ if (response == null) return undefined;
10
+ if (Object.prototype.hasOwnProperty.call(response, "result") && response.result !== undefined) {
11
+ return response.result;
12
+ }
13
+ if (Object.prototype.hasOwnProperty.call(response, "return") && response.return !== undefined) {
14
+ return response.return;
15
+ }
16
+ if (Object.prototype.hasOwnProperty.call(response, "value") && response.value !== undefined) {
17
+ return response.value;
18
+ }
19
+ return undefined;
20
+ }
21
+
22
+ function strip0x(hex) {
23
+ const s = String(hex || "");
24
+ return s.startsWith("0x") || s.startsWith("0X") ? s.slice(2) : s;
25
+ }
26
+
27
+ function ensure0x(hex) {
28
+ const s = String(hex || "");
29
+ if (!s) return s;
30
+ return s.startsWith("0x") || s.startsWith("0X") ? "0x" + s.slice(2).toLowerCase() : "0x" + s.toLowerCase();
31
+ }
32
+
33
+ /** Accept `0x…` or `cx3:…` wallet / contract ids; prefer 0x for digests. */
34
+ function normalizeAddress(addr) {
35
+ if (addr == null || addr === "") return "";
36
+ const s = String(addr).trim();
37
+ if (s.startsWith("cx3:")) return "0x" + s.slice(4).toLowerCase();
38
+ if (s.startsWith("0x") || s.startsWith("0X")) return "0x" + s.slice(2).toLowerCase();
39
+ if (/^[0-9a-fA-F]{40}$/.test(s)) return "0x" + s.toLowerCase();
40
+ return s;
41
+ }
42
+
43
+ function sleep(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ function isPlainObject(v) {
48
+ return v !== null && typeof v === "object" && !Array.isArray(v);
49
+ }
50
+
51
+ /**
52
+ * Same rules as AppSmokeTest `cx3_dapp_key_mode`:
53
+ * empty → public (any wallet); valid cx3wk_ → private (that wallet); else invalid.
54
+ */
55
+ function walletApiKeyMode(key) {
56
+ const n = String(key == null ? "" : key).trim();
57
+ if (!n) return "public";
58
+ if (/^cx3wk_[a-f0-9]{48}$/i.test(n)) return "private";
59
+ return "invalid";
60
+ }
61
+
62
+ module.exports = {
63
+ readCallValue,
64
+ strip0x,
65
+ ensure0x,
66
+ normalizeAddress,
67
+ sleep,
68
+ isPlainObject,
69
+ walletApiKeyMode
70
+ };
package/src/wallet.js ADDED
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+
3
+ const {
4
+ privateKeyToAddress,
5
+ privateKeyToPublicKey,
6
+ signMessage,
7
+ createPrivateKey
8
+ } = require("./crypto");
9
+ const { normalizeAddress } = require("./utils");
10
+ const { Cx3Error } = require("./errors");
11
+ const { buildDeployDigest, buildSendDigest, buildOwnerActionDigest, buildTransferPayload, stripNativeAddress } = require("./digest");
12
+ const { TransactionResponse } = require("./transaction");
13
+
14
+ /**
15
+ * Local secp256k1 wallet for Green Chain digests.
16
+ */
17
+ class Cx3Wallet {
18
+ /**
19
+ * @param {string|Uint8Array|Buffer} privateKey
20
+ * @param {import('./provider').Cx3Provider|null} [provider]
21
+ */
22
+ constructor(privateKey, provider = null) {
23
+ if (!privateKey) throw new Cx3Error("privateKey required", "NO_PRIVATE_KEY");
24
+ this._privateKey = privateKey;
25
+ this.provider = provider;
26
+ this.address = privateKeyToAddress(privateKey);
27
+ this.publicKey = privateKeyToPublicKey(privateKey, true).toString("hex");
28
+ }
29
+
30
+ static createRandom(provider = null) {
31
+ return new Cx3Wallet(createPrivateKey(), provider);
32
+ }
33
+
34
+ connect(provider) {
35
+ return new Cx3Wallet(this._privateKey, provider);
36
+ }
37
+
38
+ getAddress() {
39
+ return this.address;
40
+ }
41
+
42
+ async signMessage(message) {
43
+ return signMessage(message, this._privateKey);
44
+ }
45
+
46
+ async signDeploy(fields) {
47
+ const owner = normalizeAddress(fields.owner || this.address);
48
+ const timestamp = fields.timestamp || Date.now();
49
+ const digest = buildDeployDigest({
50
+ owner,
51
+ standard: fields.standard,
52
+ name: fields.name,
53
+ symbol: fields.symbol,
54
+ decimals: fields.decimals,
55
+ initialSupply: fields.initialSupply,
56
+ timestamp
57
+ });
58
+ const signed = await this.signMessage(digest);
59
+ return {
60
+ ...signed,
61
+ owner,
62
+ timestamp,
63
+ digest
64
+ };
65
+ }
66
+
67
+ async signSend(contractAddress, method, params) {
68
+ const fromAddress = this.address;
69
+ const timestamp = Date.now();
70
+ const digest = buildSendDigest(contractAddress, method, params || {}, fromAddress, timestamp);
71
+ const signed = await this.signMessage(digest);
72
+ return {
73
+ ...signed,
74
+ fromAddress,
75
+ timestamp,
76
+ digest
77
+ };
78
+ }
79
+
80
+ async signOwnerAction(action, address, owner = null) {
81
+ const own = normalizeAddress(owner || this.address);
82
+ const timestamp = Date.now();
83
+ const digest = buildOwnerActionDigest(action, address, own, timestamp);
84
+ const signed = await this.signMessage(digest);
85
+ return {
86
+ ...signed,
87
+ owner: own,
88
+ timestamp,
89
+ digest
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Signed state-changing call via attached provider.
95
+ */
96
+ async sendTransaction(contractAddress, method, params = {}, opts = {}) {
97
+ if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
98
+ const signed = await this.signSend(contractAddress, method, params);
99
+ const body = {
100
+ fromAddress: signed.fromAddress,
101
+ signature: signed.signature,
102
+ senderPublicKey: signed.senderPublicKey,
103
+ timestamp: signed.timestamp
104
+ };
105
+ const asyncMode = opts.wait === 0 || opts.async === true;
106
+ const result = await this.provider.send(contractAddress, method, params, {
107
+ ...body,
108
+ wait: asyncMode ? 0 : undefined
109
+ });
110
+ return new TransactionResponse(result, this.provider, opts);
111
+ }
112
+
113
+ /**
114
+ * Deploy via Green Chain `POST /deployContract`.
115
+ */
116
+ async deployContract(fields) {
117
+ if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
118
+ const signed = await this.signDeploy({
119
+ ...fields,
120
+ owner: fields.owner || this.address
121
+ });
122
+ const body = {
123
+ ...fields,
124
+ owner: signed.owner,
125
+ requestedBy: fields.requestedBy || signed.owner,
126
+ wallet: fields.wallet || signed.owner,
127
+ timestamp: signed.timestamp,
128
+ signature: signed.signature,
129
+ senderPublicKey: signed.senderPublicKey
130
+ };
131
+ const result = await this.provider.request("/deployContract", {
132
+ method: "POST",
133
+ body
134
+ });
135
+ return result;
136
+ }
137
+
138
+ async _ownerActionRequest(action, address, owner, method, path) {
139
+ if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
140
+ const signed = await this.signOwnerAction(action, address, owner);
141
+ return this.provider.request(path, {
142
+ method,
143
+ body: {
144
+ owner: signed.owner,
145
+ timestamp: signed.timestamp,
146
+ signature: signed.signature,
147
+ senderPublicKey: signed.senderPublicKey
148
+ }
149
+ });
150
+ }
151
+
152
+ async pauseContract(address, owner = null) {
153
+ return this._ownerActionRequest(
154
+ "pause",
155
+ address,
156
+ owner,
157
+ "PUT",
158
+ `/contracts/${encodeURIComponent(address)}/pause`
159
+ );
160
+ }
161
+
162
+ async unpauseContract(address, owner = null) {
163
+ return this._ownerActionRequest(
164
+ "unpause",
165
+ address,
166
+ owner,
167
+ "PUT",
168
+ `/contracts/${encodeURIComponent(address)}/unpause`
169
+ );
170
+ }
171
+
172
+ async deleteContract(address, owner = null) {
173
+ return this._ownerActionRequest(
174
+ "delete",
175
+ address,
176
+ owner,
177
+ "DELETE",
178
+ `/contracts/${encodeURIComponent(address)}`
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Native CX3 coin transfer (`POST /sendTransaction`).
184
+ * Distinct from `sendTransaction()` which is a signed `/contract/send`.
185
+ */
186
+ async transferNative(to, amount, opts = {}) {
187
+ if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
188
+ const toAddress = String(to || "").trim();
189
+ if (!toAddress) throw new Cx3Error("recipient required", "NO_RECIPIENT");
190
+ const amt = typeof amount === "number" ? amount : Number(amount);
191
+ if (!Number.isFinite(amt) || amt <= 0) {
192
+ throw new Cx3Error("Invalid amount", "INVALID_AMOUNT");
193
+ }
194
+ let nonce = opts.nonce;
195
+ if (nonce == null) {
196
+ try {
197
+ const pending = await this.provider.getPendingNonce(this.address);
198
+ nonce = pending && pending.nextNonce;
199
+ } catch (_) {
200
+ nonce = undefined;
201
+ }
202
+ if (nonce == null) {
203
+ nonce = await this.provider.getNonce(this.address);
204
+ }
205
+ }
206
+ const timestamp = opts.timestamp || Date.now();
207
+ const payload = buildTransferPayload({
208
+ fromAddress: this.address,
209
+ toAddress,
210
+ amount: amt,
211
+ nonce,
212
+ timestamp,
213
+ data: opts.data || null,
214
+ gasLimit: opts.gasLimit,
215
+ gasPrice: opts.gasPrice,
216
+ chainId: opts.chainId,
217
+ chainIdBind: opts.chainIdBind
218
+ });
219
+ const signed = await this.signMessage(payload);
220
+ const body = {
221
+ type: "TRANSFER",
222
+ fromAddress: stripNativeAddress(this.address),
223
+ toAddress: stripNativeAddress(toAddress),
224
+ amount: amt,
225
+ nonce,
226
+ timestamp,
227
+ data: opts.data || null,
228
+ r: signed.signature,
229
+ s: null,
230
+ v: signed.v,
231
+ publicKey: signed.publicKeyUncompressed || signed.publicKey
232
+ };
233
+ const result = await this.provider.request("/sendTransaction", {
234
+ method: "POST",
235
+ body
236
+ });
237
+ if (result && result.hash && !result.txHash) result.txHash = result.hash;
238
+ return new TransactionResponse(result, this.provider, opts);
239
+ }
240
+
241
+ async bindApiKey(address) {
242
+ if (!this.provider) throw new Cx3Error("Wallet has no provider", "NO_PROVIDER");
243
+ return this.provider.bindApiKey(address || this.address);
244
+ }
245
+ }
246
+
247
+ module.exports = { Cx3Wallet };