@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
|
@@ -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 };
|