@0xio/sdk 2.7.1 → 2.8.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/CHANGELOG.md +73 -39
- package/README.md +104 -280
- package/dist/index.d.ts +252 -44
- package/dist/index.esm.js +408 -78
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +417 -77
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +417 -77
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/index.umd.js
CHANGED
|
@@ -98,6 +98,16 @@
|
|
|
98
98
|
ErrorCode["DUPLICATE_TRANSACTION"] = "DUPLICATE_TRANSACTION";
|
|
99
99
|
ErrorCode["NONCE_TOO_FAR"] = "NONCE_TOO_FAR";
|
|
100
100
|
ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
101
|
+
// Codes the 0xio wallet returns over the bridge
|
|
102
|
+
ErrorCode["NOT_CONNECTED"] = "NOT_CONNECTED";
|
|
103
|
+
ErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS";
|
|
104
|
+
ErrorCode["METHOD_NOT_ALLOWED"] = "METHOD_NOT_ALLOWED";
|
|
105
|
+
ErrorCode["NOT_AVAILABLE"] = "NOT_AVAILABLE";
|
|
106
|
+
ErrorCode["PRIVATE_PROOF_FAILED"] = "PRIVATE_PROOF_FAILED";
|
|
107
|
+
ErrorCode["PRIVATE_TRANSFER_FAILED"] = "PRIVATE_TRANSFER_FAILED";
|
|
108
|
+
ErrorCode["CONTRACT_CALL_FAILED"] = "CONTRACT_CALL_FAILED";
|
|
109
|
+
ErrorCode["SIGN_FAILED"] = "SIGN_FAILED";
|
|
110
|
+
ErrorCode["RECIPIENT_NOT_REGISTERED"] = "RECIPIENT_NOT_REGISTERED";
|
|
101
111
|
})(exports.ErrorCode || (exports.ErrorCode = {}));
|
|
102
112
|
class ZeroXIOWalletError extends Error {
|
|
103
113
|
constructor(code, message, details) {
|
|
@@ -120,7 +130,7 @@
|
|
|
120
130
|
* window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
|
|
121
131
|
* window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
|
|
122
132
|
*
|
|
123
|
-
*
|
|
133
|
+
* Session nonce validation: injected.ts broadcasts the nonce received from the
|
|
124
134
|
* isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
|
|
125
135
|
* mismatched nonce is rejected, preventing response injection by malicious page scripts.
|
|
126
136
|
*/
|
|
@@ -153,7 +163,7 @@
|
|
|
153
163
|
window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
|
|
154
164
|
}
|
|
155
165
|
catch {
|
|
156
|
-
// Do not fall back to '*'
|
|
166
|
+
// Do not fall back to '*': silent failure is safer
|
|
157
167
|
}
|
|
158
168
|
},
|
|
159
169
|
listen(handler, options) {
|
|
@@ -168,7 +178,7 @@
|
|
|
168
178
|
...(options?.trustedParentOrigins ?? []),
|
|
169
179
|
]);
|
|
170
180
|
let _sessionNonce = null;
|
|
171
|
-
//
|
|
181
|
+
// receive the session nonce from injected.ts (MAIN world content script)
|
|
172
182
|
const nonceListener = (e) => {
|
|
173
183
|
if (e.origin !== allowedOrigin)
|
|
174
184
|
return;
|
|
@@ -191,7 +201,7 @@
|
|
|
191
201
|
return;
|
|
192
202
|
if (!e.data || e.data.source !== '0xio-sdk-bridge')
|
|
193
203
|
return;
|
|
194
|
-
//
|
|
204
|
+
// session nonce validation.
|
|
195
205
|
// Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
|
|
196
206
|
// Fallback path: if the init broadcast was missed (race between document_start
|
|
197
207
|
// content script and page script load), capture nonce from the first same-origin
|
|
@@ -261,7 +271,7 @@
|
|
|
261
271
|
amount <= Number.MAX_SAFE_INTEGER;
|
|
262
272
|
}
|
|
263
273
|
function isValidMessage(message) {
|
|
264
|
-
// Type check first
|
|
274
|
+
// Type check first: falsy non-strings (0, false, null) are not valid messages
|
|
265
275
|
if (typeof message !== 'string') {
|
|
266
276
|
return message === undefined || message === null ? true : false;
|
|
267
277
|
}
|
|
@@ -269,7 +279,7 @@
|
|
|
269
279
|
if (message.length === 0) {
|
|
270
280
|
return true;
|
|
271
281
|
}
|
|
272
|
-
// 100KB limit
|
|
282
|
+
// 100KB limit: contract call params can be large (serialized JSON)
|
|
273
283
|
return message.length <= 100000;
|
|
274
284
|
}
|
|
275
285
|
function isValidFeeLevel(feeLevel) {
|
|
@@ -302,7 +312,7 @@
|
|
|
302
312
|
}
|
|
303
313
|
/**
|
|
304
314
|
* Derive the canonical Octra address from a base64-encoded Ed25519 public key.
|
|
305
|
-
* Algorithm: SHA-256
|
|
315
|
+
* Algorithm: SHA-256 of the pubkey bytes, base58-encoded, with "oct" prepended
|
|
306
316
|
* Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
|
|
307
317
|
*/
|
|
308
318
|
async function deriveOctraAddress(publicKeyBase64) {
|
|
@@ -361,6 +371,22 @@
|
|
|
361
371
|
const microOCT = Math.round(amount * 1000000);
|
|
362
372
|
return microOCT.toString();
|
|
363
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Exact OCT to raw micro-OCT conversion using string arithmetic (no float rounding).
|
|
376
|
+
* Accepts up to 6 decimals; anything else is rejected.
|
|
377
|
+
*/
|
|
378
|
+
function octToMicro(amount) {
|
|
379
|
+
const text = typeof amount === 'number' ? amount.toFixed(6) : String(amount).trim();
|
|
380
|
+
if (!/^\d+(\.\d{1,6})?$/.test(text)) {
|
|
381
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Amount must be a positive decimal with at most 6 places');
|
|
382
|
+
}
|
|
383
|
+
const [whole, frac = ''] = text.split('.');
|
|
384
|
+
const micro = BigInt(whole) * BigInt(1000000) + BigInt(frac.padEnd(6, '0'));
|
|
385
|
+
if (micro <= BigInt(0)) {
|
|
386
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Amount must be greater than zero');
|
|
387
|
+
}
|
|
388
|
+
return micro.toString();
|
|
389
|
+
}
|
|
364
390
|
function fromMicroOCT(microAmount) {
|
|
365
391
|
const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
|
|
366
392
|
if (!Number.isFinite(amount) || amount < 0) {
|
|
@@ -389,7 +415,16 @@
|
|
|
389
415
|
[exports.ErrorCode.INVALID_SIGNATURE]: 'Invalid transaction signature',
|
|
390
416
|
[exports.ErrorCode.DUPLICATE_TRANSACTION]: 'Duplicate transaction detected',
|
|
391
417
|
[exports.ErrorCode.NONCE_TOO_FAR]: 'Transaction nonce is too far ahead',
|
|
392
|
-
[exports.ErrorCode.INTERNAL_ERROR]: 'Internal server error'
|
|
418
|
+
[exports.ErrorCode.INTERNAL_ERROR]: 'Internal server error',
|
|
419
|
+
[exports.ErrorCode.NOT_CONNECTED]: 'Connect the wallet before this request',
|
|
420
|
+
[exports.ErrorCode.INVALID_PARAMS]: 'Invalid request parameters',
|
|
421
|
+
[exports.ErrorCode.METHOD_NOT_ALLOWED]: 'This method is not permitted through the wallet',
|
|
422
|
+
[exports.ErrorCode.NOT_AVAILABLE]: 'Not available through the wallet bridge',
|
|
423
|
+
[exports.ErrorCode.PRIVATE_PROOF_FAILED]: 'Private proof generation failed',
|
|
424
|
+
[exports.ErrorCode.PRIVATE_TRANSFER_FAILED]: 'Private transfer failed',
|
|
425
|
+
[exports.ErrorCode.CONTRACT_CALL_FAILED]: 'Contract call failed',
|
|
426
|
+
[exports.ErrorCode.SIGN_FAILED]: 'Signing failed',
|
|
427
|
+
[exports.ErrorCode.RECIPIENT_NOT_REGISTERED]: 'Recipient has no private view key registered'
|
|
393
428
|
};
|
|
394
429
|
const baseMessage = baseMessages[code] || 'Unknown error';
|
|
395
430
|
return context ? `${baseMessage}: ${context}` : baseMessage;
|
|
@@ -410,7 +445,7 @@
|
|
|
410
445
|
}
|
|
411
446
|
catch (error) {
|
|
412
447
|
lastError = error;
|
|
413
|
-
// Never retry user rejections
|
|
448
|
+
// Never retry user rejections, they are intentional
|
|
414
449
|
const msg = lastError.message?.toLowerCase() || '';
|
|
415
450
|
if (msg.includes('rejected') || msg.includes('denied') || msg.includes('cancelled') || msg.includes('user refused')) {
|
|
416
451
|
throw lastError;
|
|
@@ -552,14 +587,14 @@
|
|
|
552
587
|
this._adapterReadyTeardown = null;
|
|
553
588
|
/**
|
|
554
589
|
* Set when a trusted walletReady has been received from window.parent.
|
|
555
|
-
* The polling fallback must
|
|
590
|
+
* The polling fallback must not clear this flag.
|
|
556
591
|
*/
|
|
557
592
|
this._parentTrusted = false;
|
|
558
593
|
/** walletReady postMessage listener stored for cleanup */
|
|
559
594
|
this._walletReadyMessageListener = null;
|
|
560
595
|
/**
|
|
561
596
|
* In-flight interactive request lock.
|
|
562
|
-
* Methods that open approval popups are serialized
|
|
597
|
+
* Methods that open approval popups are serialized: only one at a time.
|
|
563
598
|
*/
|
|
564
599
|
this._interactiveInFlight = false;
|
|
565
600
|
this.MAX_CONCURRENT_REQUESTS = 50;
|
|
@@ -600,9 +635,15 @@
|
|
|
600
635
|
return this.isExtensionAvailableState && this.hasExtensionContext();
|
|
601
636
|
}
|
|
602
637
|
async sendRequest(method, params = {}, timeout = 30000) {
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
|
|
638
|
+
// No-retry + long timeout for both popup/broadcast methods and long compute primitives.
|
|
639
|
+
const longOrNoRetry = ExtensionCommunicator.NO_RETRY_METHODS.has(method) ||
|
|
640
|
+
ExtensionCommunicator.LONG_COMPUTE_METHODS.has(method);
|
|
641
|
+
const maxRetries = longOrNoRetry ? 0 : 1;
|
|
642
|
+
const effectiveTimeout = ExtensionCommunicator.PROOF_METHODS.has(method)
|
|
643
|
+
? Math.max(timeout, 600000)
|
|
644
|
+
: longOrNoRetry
|
|
645
|
+
? Math.max(timeout, 180000)
|
|
646
|
+
: timeout;
|
|
606
647
|
return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
|
|
607
648
|
}
|
|
608
649
|
async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
|
|
@@ -671,7 +712,7 @@
|
|
|
671
712
|
return;
|
|
672
713
|
this._adapterTeardown = this.adapter.listen((msg) => {
|
|
673
714
|
if (msg.requestId !== undefined) {
|
|
674
|
-
// response
|
|
715
|
+
// response: map AdapterIncomingMessage to the ExtensionResponse shape
|
|
675
716
|
if (this.pendingRequests.has(msg.requestId)) {
|
|
676
717
|
this.handleExtensionResponse({
|
|
677
718
|
id: msg.requestId,
|
|
@@ -705,7 +746,7 @@
|
|
|
705
746
|
}
|
|
706
747
|
clearTimeout(pending.timeout);
|
|
707
748
|
this.pendingRequests.delete(response.id);
|
|
708
|
-
// Require strict boolean true
|
|
749
|
+
// Require strict boolean true: a "false" string or other truthy values are failures
|
|
709
750
|
if (response.success === true) {
|
|
710
751
|
pending.resolve(response.data);
|
|
711
752
|
}
|
|
@@ -730,7 +771,7 @@
|
|
|
730
771
|
}
|
|
731
772
|
postMessageToExtension(request) {
|
|
732
773
|
this.adapter.postRequest(request);
|
|
733
|
-
// Parent bridge (iframe/desktop mode)
|
|
774
|
+
// Parent bridge (iframe/desktop mode), only when a trusted origin is established.
|
|
734
775
|
// Sending with '*' would leak method + params to any intercepting frame.
|
|
735
776
|
if (window.parent !== window && this._parentOrigin) {
|
|
736
777
|
if (this.adapter.postRequestToParent) {
|
|
@@ -747,7 +788,7 @@
|
|
|
747
788
|
if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
|
|
748
789
|
throw new ZeroXIOWalletError(exports.ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
|
|
749
790
|
}
|
|
750
|
-
// Trim expired timestamps
|
|
791
|
+
// Trim expired timestamps and cap the array size to prevent unbounded growth in idle tabs
|
|
751
792
|
this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
|
|
752
793
|
if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
|
|
753
794
|
this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
|
|
@@ -767,7 +808,7 @@
|
|
|
767
808
|
const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
|
768
809
|
return `0xio-sdk-${hex}`;
|
|
769
810
|
}
|
|
770
|
-
// Crypto API unavailable
|
|
811
|
+
// Crypto API unavailable: throw rather than produce a guessable ID that
|
|
771
812
|
// could allow response spoofing via a known requestId.
|
|
772
813
|
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
|
|
773
814
|
}
|
|
@@ -781,7 +822,7 @@
|
|
|
781
822
|
this.isExtensionAvailableState = true;
|
|
782
823
|
});
|
|
783
824
|
}
|
|
784
|
-
// walletReady via postMessage (desktop/mobile iframe bridge)
|
|
825
|
+
// walletReady via postMessage (desktop/mobile iframe bridge), store the ref for cleanup
|
|
785
826
|
this._walletReadyMessageListener = (event) => {
|
|
786
827
|
if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
|
|
787
828
|
return;
|
|
@@ -816,7 +857,7 @@
|
|
|
816
857
|
};
|
|
817
858
|
window.addEventListener('message', this._walletReadyMessageListener);
|
|
818
859
|
if (window.parent !== window) {
|
|
819
|
-
this.logger.log('Running inside a frame
|
|
860
|
+
this.logger.log('Running inside a frame, waiting for the trusted walletReady signal');
|
|
820
861
|
}
|
|
821
862
|
this.checkExtensionAvailability();
|
|
822
863
|
this.extensionDetectionInterval = setInterval(() => {
|
|
@@ -825,7 +866,7 @@
|
|
|
825
866
|
}
|
|
826
867
|
checkExtensionAvailability() {
|
|
827
868
|
// If parent-bridge readiness was established via a trusted walletReady handshake,
|
|
828
|
-
// preserve that state
|
|
869
|
+
// preserve that state: the polling fallback (detectExtensionSignals) does not
|
|
829
870
|
// consider the iframe parent signal and would incorrectly flip state back
|
|
830
871
|
if (this._parentTrusted) {
|
|
831
872
|
return;
|
|
@@ -907,7 +948,7 @@
|
|
|
907
948
|
}
|
|
908
949
|
/**
|
|
909
950
|
* Clean up SDK resources.
|
|
910
|
-
* After cleanup() the instance is terminal
|
|
951
|
+
* After cleanup() the instance is terminal: do not call initialize() again.
|
|
911
952
|
* Construct a new instance instead.
|
|
912
953
|
*/
|
|
913
954
|
cleanup() {
|
|
@@ -948,7 +989,7 @@
|
|
|
948
989
|
};
|
|
949
990
|
}
|
|
950
991
|
}
|
|
951
|
-
// Methods that trigger user-facing popups
|
|
992
|
+
// Methods that trigger user-facing popups: never retry these.
|
|
952
993
|
// Retrying sends a second request while the first popup is still open,
|
|
953
994
|
// causing double popups where the second tx fails (stale nonce/state).
|
|
954
995
|
ExtensionCommunicator.NO_RETRY_METHODS = new Set([
|
|
@@ -956,12 +997,29 @@
|
|
|
956
997
|
'sign_transaction', 'broadcast_only',
|
|
957
998
|
'send_private_transfer', 'claim_private_transfer',
|
|
958
999
|
'encrypt_balance', 'decrypt_balance',
|
|
1000
|
+
// Broadcasts a sequence of contract txs, so it must never be retried (it would double-send).
|
|
1001
|
+
'send_contract_transaction_sequence',
|
|
1002
|
+
// Shows an approval popup + broadcasts a registration tx (offscreen cold-init can exceed
|
|
1003
|
+
// 30s): interactive, long timeout, no retry.
|
|
1004
|
+
'register_private_view_key',
|
|
1005
|
+
]);
|
|
1006
|
+
// Long-running compute primitives (RFP): proof generation / decrypt take 10-120s, so
|
|
1007
|
+
// they need the long (180s) timeout and must not be retried (a retry wastes ~a minute of
|
|
1008
|
+
// compute). They do not show approval popups, so, unlike NO_RETRY_METHODS, they are not
|
|
1009
|
+
// subject to the one-at-a-time interactive lock (a dapp may run several concurrently).
|
|
1010
|
+
ExtensionCommunicator.LONG_COMPUTE_METHODS = new Set([
|
|
1011
|
+
'make_zero_proof', 'make_range_proof', 'decrypt_value',
|
|
1012
|
+
'get_private_balance', 'encrypt_value',
|
|
959
1013
|
]);
|
|
1014
|
+
// A private transfer runs proof generation after the approval (minutes on a slow machine),
|
|
1015
|
+
// so its wait is longer than the popup window alone.
|
|
1016
|
+
ExtensionCommunicator.PROOF_METHODS = new Set(['send_private_transfer']);
|
|
1017
|
+
// The interactive lock applies only to methods that open a wallet popup / broadcast.
|
|
960
1018
|
ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
|
|
961
1019
|
// only forward known event types
|
|
962
1020
|
ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
|
|
963
1021
|
'connect', 'disconnect', 'accountChanged', 'balanceChanged',
|
|
964
|
-
'networkChanged', 'transactionConfirmed', 'permissionsChanged', 'message',
|
|
1022
|
+
'networkChanged', 'transactionConfirmed', 'transactionFailed', 'permissionsChanged', 'message',
|
|
965
1023
|
'error', 'extensionLocked', 'extensionUnlocked'
|
|
966
1024
|
]);
|
|
967
1025
|
|
|
@@ -983,7 +1041,7 @@
|
|
|
983
1041
|
'devnet': {
|
|
984
1042
|
id: 'devnet',
|
|
985
1043
|
name: 'Octra Devnet',
|
|
986
|
-
rpcUrl: '
|
|
1044
|
+
rpcUrl: 'https://devnet.octrascan.io',
|
|
987
1045
|
explorerUrl: 'https://devnet.octrascan.io/tx.html?hash=',
|
|
988
1046
|
explorerAddressUrl: 'https://devnet.octrascan.io/address.html?addr=',
|
|
989
1047
|
indexerUrl: 'https://devnet.octrascan.io',
|
|
@@ -1011,7 +1069,7 @@
|
|
|
1011
1069
|
const DEFAULT_NETWORK_ID = 'mainnet';
|
|
1012
1070
|
/**
|
|
1013
1071
|
* Get network configuration by ID.
|
|
1014
|
-
* Returns a frozen copy
|
|
1072
|
+
* Returns a frozen copy, so callers cannot mutate SDK-internal state.
|
|
1015
1073
|
*/
|
|
1016
1074
|
function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
|
|
1017
1075
|
if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
|
|
@@ -1021,7 +1079,7 @@
|
|
|
1021
1079
|
}
|
|
1022
1080
|
/**
|
|
1023
1081
|
* Get all available networks.
|
|
1024
|
-
* Returns frozen copies
|
|
1082
|
+
* Returns frozen copies, so callers cannot mutate SDK-internal state.
|
|
1025
1083
|
*/
|
|
1026
1084
|
function getAllNetworks() {
|
|
1027
1085
|
return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
|
|
@@ -1070,7 +1128,7 @@
|
|
|
1070
1128
|
|
|
1071
1129
|
/**
|
|
1072
1130
|
* Default balance structure.
|
|
1073
|
-
* Accepts a numeric total or undefined
|
|
1131
|
+
* Accepts a numeric total or undefined. Never pass a Balance object here.
|
|
1074
1132
|
*/
|
|
1075
1133
|
function createDefaultBalance(total = 0) {
|
|
1076
1134
|
const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
|
|
@@ -1089,7 +1147,7 @@
|
|
|
1089
1147
|
if (raw === null || raw === undefined)
|
|
1090
1148
|
return null;
|
|
1091
1149
|
// If it's already a Balance-shaped object, extract numeric fields
|
|
1092
|
-
// Use Number() not parseFloat()
|
|
1150
|
+
// Use Number() not parseFloat(): parseFloat('10abc') silently returns 10
|
|
1093
1151
|
const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
|
|
1094
1152
|
const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
|
|
1095
1153
|
if (!Number.isFinite(pub) || pub < 0)
|
|
@@ -1104,7 +1162,7 @@
|
|
|
1104
1162
|
};
|
|
1105
1163
|
}
|
|
1106
1164
|
const SDK_CONFIG = {
|
|
1107
|
-
version: '2.
|
|
1165
|
+
version: '2.8.0',
|
|
1108
1166
|
defaultNetworkId: DEFAULT_NETWORK_ID,
|
|
1109
1167
|
communicationTimeout: 30000, // 30 seconds
|
|
1110
1168
|
retryAttempts: 3,
|
|
@@ -1114,13 +1172,154 @@
|
|
|
1114
1172
|
return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
|
|
1115
1173
|
}
|
|
1116
1174
|
|
|
1175
|
+
/** Scope names the 0xio wallet enforces. Any other name is dropped at connect. */
|
|
1176
|
+
const WALLET_PERMISSIONS = [
|
|
1177
|
+
'accounts',
|
|
1178
|
+
'public_transactions',
|
|
1179
|
+
'contract_calls',
|
|
1180
|
+
'contract_views',
|
|
1181
|
+
'private_balance_read',
|
|
1182
|
+
'private_proofs',
|
|
1183
|
+
'private_transfers',
|
|
1184
|
+
'private_claims',
|
|
1185
|
+
];
|
|
1186
|
+
/** Older SDK permission names and the wallet scope each one means. */
|
|
1187
|
+
const LEGACY_PERMISSION_MAP = {
|
|
1188
|
+
read_address: 'accounts',
|
|
1189
|
+
read_balance: 'accounts',
|
|
1190
|
+
read_public_key: 'accounts',
|
|
1191
|
+
send_transactions: 'public_transactions',
|
|
1192
|
+
sign_messages: 'public_transactions',
|
|
1193
|
+
contract_calls: 'contract_calls',
|
|
1194
|
+
view_private_balance: 'private_balance_read',
|
|
1195
|
+
view_encrypted_balance: 'private_balance_read',
|
|
1196
|
+
stealth_scan: 'private_balance_read',
|
|
1197
|
+
decrypt_balance: 'private_balance_read',
|
|
1198
|
+
encrypt_balance: 'private_proofs',
|
|
1199
|
+
private_transfers: 'private_transfers',
|
|
1200
|
+
stealth_claim: 'private_claims',
|
|
1201
|
+
};
|
|
1202
|
+
/** Translate any mix of old and new names into the wallet's scope names, without duplicates. */
|
|
1203
|
+
function toWalletPermissions(perms) {
|
|
1204
|
+
const out = [];
|
|
1205
|
+
for (const p of perms ?? []) {
|
|
1206
|
+
const canonical = WALLET_PERMISSIONS.includes(p)
|
|
1207
|
+
? p
|
|
1208
|
+
: LEGACY_PERMISSION_MAP[p];
|
|
1209
|
+
if (canonical && !out.includes(canonical))
|
|
1210
|
+
out.push(canonical);
|
|
1211
|
+
}
|
|
1212
|
+
return out;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* The granted wallet scopes plus every requested old name they satisfy, so a dapp that checks
|
|
1216
|
+
* for the name it asked for (for example 'read_balance') keeps seeing it.
|
|
1217
|
+
*/
|
|
1218
|
+
function withLegacyAliases(granted, requested) {
|
|
1219
|
+
const set = new Set(granted ?? []);
|
|
1220
|
+
for (const p of requested ?? []) {
|
|
1221
|
+
const canonical = LEGACY_PERMISSION_MAP[p];
|
|
1222
|
+
if (canonical && set.has(canonical))
|
|
1223
|
+
set.add(p);
|
|
1224
|
+
}
|
|
1225
|
+
return [...set];
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* 0xio Signed Message standard (v1).
|
|
1230
|
+
*
|
|
1231
|
+
* `wallet.signMessage(message)` never signs the raw message. The wallet frames it first so a signed
|
|
1232
|
+
* "message" can never collide with a transaction pre-image (a transaction is canonical JSON that
|
|
1233
|
+
* begins with '{'). The framing is:
|
|
1234
|
+
*
|
|
1235
|
+
* "Octra Signed Message:\n" + utf8ByteLength(message) + "\n" + message
|
|
1236
|
+
*
|
|
1237
|
+
* signed as an Ed25519 detached signature over the UTF-8 bytes of that string. The leading 'O'
|
|
1238
|
+
* guarantees the signed bytes never begin with '{', so a personal-message signature can never be a
|
|
1239
|
+
* valid transaction. Any verifier MUST reconstruct the same bytes: use `getSignedMessageBytes()`
|
|
1240
|
+
* with any Ed25519 library, or `verifyMessage()` for a batteries-included check.
|
|
1241
|
+
*/
|
|
1242
|
+
/** Fixed prefix tag for the 0xio Signed Message scheme. */
|
|
1243
|
+
const SIGNED_MESSAGE_PREFIX = 'Octra Signed Message:';
|
|
1244
|
+
/** Scheme version, bumped if the framing ever changes so verifiers can detect it. */
|
|
1245
|
+
const SIGNED_MESSAGE_VERSION = 1;
|
|
1246
|
+
/** UTF-8 byte length of a string (JS `.length` counts UTF-16 units, not bytes). */
|
|
1247
|
+
function utf8ByteLength(s) {
|
|
1248
|
+
return new TextEncoder().encode(s).length;
|
|
1249
|
+
}
|
|
1250
|
+
function base64ToBytes(b64) {
|
|
1251
|
+
// atob is available in browsers and Node 16+; utils.deriveOctraAddress uses the same path.
|
|
1252
|
+
const bin = atob(b64);
|
|
1253
|
+
const out = new Uint8Array(bin.length);
|
|
1254
|
+
for (let i = 0; i < bin.length; i++)
|
|
1255
|
+
out[i] = bin.charCodeAt(i);
|
|
1256
|
+
return out;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* The exact bytes that `wallet.signMessage(message)` produces a signature over. Verify a 0xio
|
|
1260
|
+
* message signature by checking an Ed25519 signature against these bytes with the signer's public
|
|
1261
|
+
* key. Zero-dependency - bring your own Ed25519 verifier, or use `verifyMessage`.
|
|
1262
|
+
*/
|
|
1263
|
+
function getSignedMessageBytes(message) {
|
|
1264
|
+
if (typeof message !== 'string') {
|
|
1265
|
+
throw new TypeError('message must be a string');
|
|
1266
|
+
}
|
|
1267
|
+
const framed = `${SIGNED_MESSAGE_PREFIX}\n${utf8ByteLength(message)}\n${message}`;
|
|
1268
|
+
return new TextEncoder().encode(framed);
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Reconstruct the auth message that `wallet.signAuthMessage(service, nonce)` signs. A relying
|
|
1272
|
+
* service verifies an auth signature with `verifyMessage(buildAuthMessage(service, nonce, origin),
|
|
1273
|
+
* signature, publicKey)`, where `origin` is the caller's page origin.
|
|
1274
|
+
*/
|
|
1275
|
+
function buildAuthMessage(service, nonce, origin) {
|
|
1276
|
+
return `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Verify a 0xio message signature produced by `wallet.signMessage`.
|
|
1280
|
+
*
|
|
1281
|
+
* @param message The original message passed to `wallet.signMessage`.
|
|
1282
|
+
* @param signature Base64 Ed25519 signature returned by `wallet.signMessage`.
|
|
1283
|
+
* @param publicKey Base64 Ed25519 public key of the signer (from `wallet.getPublicKey()`).
|
|
1284
|
+
* @returns Whether the signature is valid for this message and key.
|
|
1285
|
+
*
|
|
1286
|
+
* Uses the Web Crypto Ed25519 primitive (Node 18+, Chrome 137+, Safari 17+, Firefox 129+). In an
|
|
1287
|
+
* environment without it, verify `getSignedMessageBytes(message)` with your own Ed25519 library.
|
|
1288
|
+
*/
|
|
1289
|
+
async function verifyMessage(message, signature, publicKey) {
|
|
1290
|
+
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
1291
|
+
throw new Error('Web Crypto API unavailable; verify getSignedMessageBytes(message) with your own Ed25519 library');
|
|
1292
|
+
}
|
|
1293
|
+
let sigBytes;
|
|
1294
|
+
let pubBytes;
|
|
1295
|
+
try {
|
|
1296
|
+
sigBytes = base64ToBytes(signature);
|
|
1297
|
+
pubBytes = base64ToBytes(publicKey);
|
|
1298
|
+
}
|
|
1299
|
+
catch {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
const data = new Uint8Array(getSignedMessageBytes(message));
|
|
1304
|
+
const key = await crypto.subtle.importKey('raw', pubBytes, { name: 'Ed25519' }, false, [
|
|
1305
|
+
'verify',
|
|
1306
|
+
]);
|
|
1307
|
+
return await crypto.subtle.verify({ name: 'Ed25519' }, key, sigBytes, data);
|
|
1308
|
+
}
|
|
1309
|
+
catch {
|
|
1310
|
+
// importKey/verify can throw (rather than return false) on malformed input or an environment
|
|
1311
|
+
// that recognizes Ed25519 only partially; treat any such failure as "not verified".
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1117
1316
|
class ZeroXIOWallet extends EventEmitter {
|
|
1118
1317
|
constructor(config) {
|
|
1119
1318
|
super(config.debug);
|
|
1120
1319
|
this.connectionInfo = { isConnected: false };
|
|
1121
1320
|
this.isInitialized = false;
|
|
1122
1321
|
this._initPromise = null;
|
|
1123
|
-
// session version
|
|
1322
|
+
// session version, for stale write detection
|
|
1124
1323
|
this._sessionVersion = 0;
|
|
1125
1324
|
this.config = {
|
|
1126
1325
|
...config,
|
|
@@ -1151,7 +1350,7 @@
|
|
|
1151
1350
|
appVersion: this.config.appVersion,
|
|
1152
1351
|
appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
1153
1352
|
appIcon: this.config.appIcon,
|
|
1154
|
-
requiredPermissions: this.config.requiredPermissions,
|
|
1353
|
+
requiredPermissions: toWalletPermissions(this.config.requiredPermissions),
|
|
1155
1354
|
networkId: this.config.networkId
|
|
1156
1355
|
});
|
|
1157
1356
|
this.setupExtensionEventListeners();
|
|
@@ -1179,22 +1378,22 @@
|
|
|
1179
1378
|
this.ensureInitialized();
|
|
1180
1379
|
try {
|
|
1181
1380
|
this.logger.log('Attempting to connect with options:', options);
|
|
1182
|
-
// filter to declared perms only
|
|
1381
|
+
// filter to declared perms only: accept both RFC 'permissions' and legacy 'requestPermissions'
|
|
1183
1382
|
const declaredPermissions = this.config.requiredPermissions || [];
|
|
1184
1383
|
const requestedPerms = options.permissions ?? options.requestPermissions;
|
|
1185
1384
|
const requestedPermissions = requestedPerms
|
|
1186
1385
|
? requestedPerms.filter(p => declaredPermissions.includes(p))
|
|
1187
1386
|
: declaredPermissions;
|
|
1188
1387
|
const result = await this.communicator.sendRequest('connect', {
|
|
1189
|
-
permissions: requestedPermissions,
|
|
1388
|
+
permissions: toWalletPermissions(requestedPermissions),
|
|
1190
1389
|
networkId: options.networkId || this.config.networkId
|
|
1191
1390
|
});
|
|
1192
|
-
// verify
|
|
1391
|
+
// verify the public key to address binding
|
|
1193
1392
|
if (result.publicKey && result.address) {
|
|
1194
1393
|
try {
|
|
1195
1394
|
const derived = await deriveOctraAddress(result.publicKey);
|
|
1196
1395
|
if (derived !== result.address) {
|
|
1197
|
-
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed
|
|
1396
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed: the reported public key does not derive to the reported address');
|
|
1198
1397
|
}
|
|
1199
1398
|
}
|
|
1200
1399
|
catch (e) {
|
|
@@ -1203,14 +1402,16 @@
|
|
|
1203
1402
|
this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
|
|
1204
1403
|
}
|
|
1205
1404
|
}
|
|
1206
|
-
// Use networkInfo from extension response
|
|
1405
|
+
// Use networkInfo from the extension response, validated before caching.
|
|
1207
1406
|
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1208
1407
|
?? (result.networkId ? getNetworkConfig(result.networkId) : null);
|
|
1209
1408
|
if (!networkInfo) {
|
|
1210
1409
|
throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
|
|
1211
1410
|
}
|
|
1212
|
-
|
|
1213
|
-
//
|
|
1411
|
+
// The wallet answers with its own scope names; the names the dapp asked for are kept as
|
|
1412
|
+
// aliases so existing permission checks keep working.
|
|
1413
|
+
const permissions = withLegacyAliases(result.permissions, requestedPermissions);
|
|
1414
|
+
// Update connection info, including permissions
|
|
1214
1415
|
this.connectionInfo = {
|
|
1215
1416
|
isConnected: true,
|
|
1216
1417
|
address: result.address,
|
|
@@ -1284,12 +1485,12 @@
|
|
|
1284
1485
|
if (this._sessionVersion !== sv)
|
|
1285
1486
|
return { ...this.connectionInfo };
|
|
1286
1487
|
if (result.isConnected && result.address) {
|
|
1287
|
-
// verify
|
|
1488
|
+
// verify the public key to address binding
|
|
1288
1489
|
if (result.publicKey) {
|
|
1289
1490
|
try {
|
|
1290
1491
|
const derived = await deriveOctraAddress(result.publicKey);
|
|
1291
1492
|
if (derived !== result.address) {
|
|
1292
|
-
this.logger.warn('Address-key binding mismatch on session restore
|
|
1493
|
+
this.logger.warn('Address-key binding mismatch on session restore, ignoring stale session');
|
|
1293
1494
|
this.connectionInfo = { isConnected: false };
|
|
1294
1495
|
return { ...this.connectionInfo };
|
|
1295
1496
|
}
|
|
@@ -1303,11 +1504,11 @@
|
|
|
1303
1504
|
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1304
1505
|
?? (result.networkId ? getNetworkConfig(result.networkId) : null);
|
|
1305
1506
|
if (!networkInfo) {
|
|
1306
|
-
this.logger.warn('getConnectionStatus: wallet returned no network metadata
|
|
1507
|
+
this.logger.warn('getConnectionStatus: wallet returned no network metadata, returning cached state');
|
|
1307
1508
|
return this.connectionInfo;
|
|
1308
1509
|
}
|
|
1309
1510
|
const wasConnected = this.connectionInfo.isConnected;
|
|
1310
|
-
const permissions = result.permissions
|
|
1511
|
+
const permissions = withLegacyAliases(result.permissions, this.config.requiredPermissions);
|
|
1311
1512
|
// preserve existing connectedAt
|
|
1312
1513
|
const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
|
|
1313
1514
|
this.connectionInfo = {
|
|
@@ -1320,7 +1521,7 @@
|
|
|
1320
1521
|
permissions
|
|
1321
1522
|
};
|
|
1322
1523
|
this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
|
|
1323
|
-
// only emit on disconnected
|
|
1524
|
+
// only emit on the disconnected to connected transition
|
|
1324
1525
|
if (!wasConnected) {
|
|
1325
1526
|
const connectEvent = {
|
|
1326
1527
|
address: result.address,
|
|
@@ -1345,8 +1546,8 @@
|
|
|
1345
1546
|
}
|
|
1346
1547
|
}
|
|
1347
1548
|
/**
|
|
1348
|
-
* Switch the extension's active network (e.g. 'mainnet'
|
|
1349
|
-
* Works silently
|
|
1549
|
+
* Switch the extension's active network (e.g. 'mainnet' to 'devnet').
|
|
1550
|
+
* Works silently: no popup, no user confirmation needed.
|
|
1350
1551
|
* The extension broadcasts 'networkChanged' event to all connected dApps.
|
|
1351
1552
|
*/
|
|
1352
1553
|
async switchNetwork(networkId) {
|
|
@@ -1381,6 +1582,22 @@
|
|
|
1381
1582
|
getAddress() {
|
|
1382
1583
|
return this.connectionInfo.address || null;
|
|
1383
1584
|
}
|
|
1585
|
+
/**
|
|
1586
|
+
* The connected account's Ed25519 public key (base64). Served from the session when the
|
|
1587
|
+
* wallet reported it at connect, otherwise asked from the wallet.
|
|
1588
|
+
*/
|
|
1589
|
+
async getPublicKey() {
|
|
1590
|
+
this.ensureConnected();
|
|
1591
|
+
if (this.connectionInfo.publicKey)
|
|
1592
|
+
return this.connectionInfo.publicKey;
|
|
1593
|
+
const result = await this.communicator.sendRequest('getPublicKey');
|
|
1594
|
+
const publicKey = result?.publicKey;
|
|
1595
|
+
if (typeof publicKey !== 'string' || !publicKey) {
|
|
1596
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Wallet did not return a public key');
|
|
1597
|
+
}
|
|
1598
|
+
this.connectionInfo.publicKey = publicKey;
|
|
1599
|
+
return publicKey;
|
|
1600
|
+
}
|
|
1384
1601
|
async getBalance(forceRefresh = false) {
|
|
1385
1602
|
this.ensureConnected();
|
|
1386
1603
|
try {
|
|
@@ -1470,16 +1687,14 @@
|
|
|
1470
1687
|
if (!isValidAddress(txData.to)) {
|
|
1471
1688
|
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1472
1689
|
}
|
|
1473
|
-
|
|
1474
|
-
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1475
|
-
}
|
|
1690
|
+
const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
|
|
1476
1691
|
if (txData.message && txData.message.length > 1000) {
|
|
1477
1692
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1478
1693
|
}
|
|
1479
1694
|
try {
|
|
1480
1695
|
// log non-sensitive only
|
|
1481
1696
|
this.logger.log('Sending transaction:', { to: txData.to });
|
|
1482
|
-
const result = await this.communicator.sendRequest('send_transaction', txData);
|
|
1697
|
+
const result = await this.communicator.sendRequest('send_transaction', { ...txData, amount });
|
|
1483
1698
|
this.logger.log('Transaction result:', result);
|
|
1484
1699
|
// Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
|
|
1485
1700
|
if (result.accepted ?? result.success) {
|
|
@@ -1508,15 +1723,13 @@
|
|
|
1508
1723
|
if (!isValidAddress(txData.to)) {
|
|
1509
1724
|
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1510
1725
|
}
|
|
1511
|
-
|
|
1512
|
-
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1513
|
-
}
|
|
1726
|
+
const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
|
|
1514
1727
|
if (txData.message && txData.message.length > 1000) {
|
|
1515
1728
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1516
1729
|
}
|
|
1517
1730
|
try {
|
|
1518
1731
|
this.logger.log('Requesting transaction signature:', { to: txData.to });
|
|
1519
|
-
const result = await this.communicator.sendRequest('sign_transaction', txData);
|
|
1732
|
+
const result = await this.communicator.sendRequest('sign_transaction', { ...txData, amount });
|
|
1520
1733
|
return result;
|
|
1521
1734
|
}
|
|
1522
1735
|
catch (error) {
|
|
@@ -1560,9 +1773,15 @@
|
|
|
1560
1773
|
if (callData.method.length > 200) {
|
|
1561
1774
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
|
|
1562
1775
|
}
|
|
1776
|
+
if (callData.amount != null && callData.amountOct != null) {
|
|
1777
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Contract call amount: pass amount or amountOct, not both');
|
|
1778
|
+
}
|
|
1563
1779
|
if (callData.amount != null) {
|
|
1564
1780
|
this.assertExactOCTAmount(callData.amount, 'Contract call amount');
|
|
1565
1781
|
}
|
|
1782
|
+
if (callData.amountOct != null) {
|
|
1783
|
+
this.assertExactOCTAmount(callData.amountOct, 'Contract call amount');
|
|
1784
|
+
}
|
|
1566
1785
|
try {
|
|
1567
1786
|
if (JSON.stringify(callData.params).length > 65536) {
|
|
1568
1787
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
|
|
@@ -1580,7 +1799,11 @@
|
|
|
1580
1799
|
contract: callData.contract,
|
|
1581
1800
|
method: callData.method,
|
|
1582
1801
|
params: callData.params,
|
|
1583
|
-
amount: callData.
|
|
1802
|
+
amount: callData.amountOct != null
|
|
1803
|
+
? octToMicro(callData.amountOct)
|
|
1804
|
+
: callData.amount != null
|
|
1805
|
+
? String(callData.amount)
|
|
1806
|
+
: '0',
|
|
1584
1807
|
ou: callData.ou != null ? String(callData.ou) : '10000',
|
|
1585
1808
|
});
|
|
1586
1809
|
this.logger.log('Contract call result:', result);
|
|
@@ -1697,7 +1920,9 @@
|
|
|
1697
1920
|
}
|
|
1698
1921
|
}
|
|
1699
1922
|
/**
|
|
1700
|
-
* Encrypt public balance to private
|
|
1923
|
+
* Encrypt public balance to private.
|
|
1924
|
+
* @deprecated The 0xio extension does not serve this through the bridge (it answers
|
|
1925
|
+
* NOT_AVAILABLE); users encrypt from the extension's Privacy screen.
|
|
1701
1926
|
*/
|
|
1702
1927
|
async encryptBalance(amount) {
|
|
1703
1928
|
this.ensureConnected();
|
|
@@ -1718,7 +1943,9 @@
|
|
|
1718
1943
|
}
|
|
1719
1944
|
}
|
|
1720
1945
|
/**
|
|
1721
|
-
* Decrypt private balance to public
|
|
1946
|
+
* Decrypt private balance to public.
|
|
1947
|
+
* @deprecated The 0xio extension does not serve this through the bridge (it answers
|
|
1948
|
+
* NOT_AVAILABLE); users decrypt from the extension's Privacy screen.
|
|
1722
1949
|
*/
|
|
1723
1950
|
async decryptBalance(amount) {
|
|
1724
1951
|
this.ensureConnected();
|
|
@@ -1760,7 +1987,10 @@
|
|
|
1760
1987
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
|
|
1761
1988
|
}
|
|
1762
1989
|
try {
|
|
1763
|
-
const result = await this.communicator.sendRequest('send_private_transfer',
|
|
1990
|
+
const result = await this.communicator.sendRequest('send_private_transfer', {
|
|
1991
|
+
...transferData,
|
|
1992
|
+
...(transferData.amountRaw ? { amount_raw: transferData.amountRaw } : {}),
|
|
1993
|
+
});
|
|
1764
1994
|
// Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
|
|
1765
1995
|
if (result.accepted ?? result.success) {
|
|
1766
1996
|
setTimeout(() => {
|
|
@@ -1814,16 +2044,89 @@
|
|
|
1814
2044
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
|
|
1815
2045
|
}
|
|
1816
2046
|
}
|
|
2047
|
+
// Generic provider passthrough and RFP private primitives (since 2.8.0)
|
|
2048
|
+
// These route through the wallet bridge. The wallet keeps all private/FHE
|
|
2049
|
+
// secret material internal and returns only ciphertexts/proofs/tx hashes.
|
|
2050
|
+
/**
|
|
2051
|
+
* Send any wallet method + params through the bridge. Escape hatch for
|
|
2052
|
+
* primitives that don't have a typed helper yet (no SDK upgrade needed).
|
|
2053
|
+
* @since 2.8.0
|
|
2054
|
+
*/
|
|
2055
|
+
async request(method, params = {}) {
|
|
2056
|
+
return this.communicator.sendRequest(method, params);
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Read-only node RPC through the wallet. Only the wallet's allow-listed public methods work
|
|
2060
|
+
* (octra_balance, octra_transaction, contract_call and similar); writes are refused.
|
|
2061
|
+
* @since 2.8.0
|
|
2062
|
+
*/
|
|
2063
|
+
async rpcCall(method, params = []) {
|
|
2064
|
+
return this.communicator.sendRequest('rpc_call', { method, params });
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Feature-detect which private capabilities the connected wallet supports.
|
|
2068
|
+
* Lets a dapp render the correct UI (or fail closed) before any action.
|
|
2069
|
+
* @since 2.8.0
|
|
2070
|
+
*/
|
|
2071
|
+
async getPrivateCapabilities() {
|
|
2072
|
+
return this.communicator.sendRequest('get_private_capabilities');
|
|
2073
|
+
}
|
|
2074
|
+
/** Read-only contract view (no approval popup). @since 2.8.0 */
|
|
2075
|
+
async callContractView(params) {
|
|
2076
|
+
this.ensureConnected();
|
|
2077
|
+
return this.communicator.sendRequest('contract_call_view', params);
|
|
2078
|
+
}
|
|
2079
|
+
/** Encrypt a raw integer value to a contract-ready ciphertext (keys stay in wallet). @since 2.8.0 */
|
|
2080
|
+
async encryptValue(params) {
|
|
2081
|
+
this.ensureConnected();
|
|
2082
|
+
return this.communicator.sendRequest('encrypt_value', params);
|
|
2083
|
+
}
|
|
2084
|
+
/** Decrypt a ciphertext to a raw integer value. @since 2.8.0 */
|
|
2085
|
+
async decryptValue(params) {
|
|
2086
|
+
this.ensureConnected();
|
|
2087
|
+
return this.communicator.sendRequest('decrypt_value', params);
|
|
2088
|
+
}
|
|
2089
|
+
/** Zero proof for a ciphertext (proves it encrypts the given value, default 0). @since 2.8.0 */
|
|
2090
|
+
async makeZeroProof(params) {
|
|
2091
|
+
this.ensureConnected();
|
|
2092
|
+
return this.communicator.sendRequest('make_zero_proof', params);
|
|
2093
|
+
}
|
|
2094
|
+
/** Range proof for a ciphertext. @since 2.8.0 */
|
|
2095
|
+
async makeRangeProof(params) {
|
|
2096
|
+
this.ensureConnected();
|
|
2097
|
+
return this.communicator.sendRequest('make_range_proof', params);
|
|
2098
|
+
}
|
|
2099
|
+
/** Private OCT + private token balances (decrypted inside the wallet). @since 2.8.0 */
|
|
2100
|
+
async getPrivateBalance(params = {}) {
|
|
2101
|
+
this.ensureConnected();
|
|
2102
|
+
return this.communicator.sendRequest('get_private_balance', params);
|
|
2103
|
+
}
|
|
2104
|
+
/** Register a receive/view public key so an address can receive private transfers. @since 2.8.0 */
|
|
2105
|
+
async registerPrivateViewKey(params) {
|
|
2106
|
+
this.ensureConnected();
|
|
2107
|
+
return this.communicator.sendRequest('register_private_view_key', params);
|
|
2108
|
+
}
|
|
2109
|
+
/** Submit an ordered multi-step public contract flow under one approval. @since 2.8.0 */
|
|
2110
|
+
async sendContractTransactionSequence(params) {
|
|
2111
|
+
this.ensureConnected();
|
|
2112
|
+
return this.communicator.sendRequest('send_contract_transaction_sequence', params);
|
|
2113
|
+
}
|
|
1817
2114
|
/**
|
|
1818
|
-
* Sign an arbitrary message with the wallet's private key
|
|
1819
|
-
* The user will be prompted to approve the signature request in the extension
|
|
2115
|
+
* Sign an arbitrary message with the wallet's private key.
|
|
2116
|
+
* The user will be prompted to approve the signature request in the extension.
|
|
2117
|
+
*
|
|
2118
|
+
* The wallet does not sign the raw message: it signs the 0xio Signed Message framing
|
|
2119
|
+
* (`"Octra Signed Message:\n" + byteLength + "\n" + message`) so a signed message can never be a
|
|
2120
|
+
* transaction pre-image. Verify with `verifyMessage(message, signature, publicKey)`, or check an
|
|
2121
|
+
* Ed25519 signature against `getSignedMessageBytes(message)` with your own library.
|
|
2122
|
+
*
|
|
1820
2123
|
* @param message - The message to sign (non-empty string)
|
|
1821
2124
|
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1822
2125
|
* @throws ZeroXIOWalletError with code SIGNATURE_FAILED if signing fails
|
|
1823
2126
|
* @example
|
|
1824
2127
|
* ```typescript
|
|
1825
2128
|
* const signature = await wallet.signMessage('Hello, 0xio!');
|
|
1826
|
-
*
|
|
2129
|
+
* const ok = await verifyMessage('Hello, 0xio!', signature, await wallet.getPublicKey());
|
|
1827
2130
|
* ```
|
|
1828
2131
|
*/
|
|
1829
2132
|
async signMessage(message) {
|
|
@@ -1857,7 +2160,7 @@
|
|
|
1857
2160
|
* to the calling service and a one-time nonce, preventing cross-service replay attacks.
|
|
1858
2161
|
*
|
|
1859
2162
|
* @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
|
|
1860
|
-
* @param nonce - Unique one-time value
|
|
2163
|
+
* @param nonce - Unique one-time value. Use a server-generated UUID or challenge
|
|
1861
2164
|
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1862
2165
|
*/
|
|
1863
2166
|
async signAuthMessage(service, nonce) {
|
|
@@ -1869,8 +2172,7 @@
|
|
|
1869
2172
|
throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
|
|
1870
2173
|
}
|
|
1871
2174
|
const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
|
|
1872
|
-
|
|
1873
|
-
return this.signMessage(domainSeparated);
|
|
2175
|
+
return this.signMessage(buildAuthMessage(service, nonce, origin));
|
|
1874
2176
|
}
|
|
1875
2177
|
ensureInitialized() {
|
|
1876
2178
|
if (!this.isInitialized) {
|
|
@@ -1902,6 +2204,9 @@
|
|
|
1902
2204
|
this.communicator.on('transactionConfirmed', (event) => {
|
|
1903
2205
|
this.handleTransactionConfirmed(event.data);
|
|
1904
2206
|
});
|
|
2207
|
+
this.communicator.on('transactionFailed', (event) => {
|
|
2208
|
+
this.emit('transactionFailed', event.data ?? event);
|
|
2209
|
+
});
|
|
1905
2210
|
this.communicator.on('permissionsChanged', (event) => {
|
|
1906
2211
|
const permissions = event.data ?? event;
|
|
1907
2212
|
if (this.connectionInfo.isConnected) {
|
|
@@ -1940,7 +2245,7 @@
|
|
|
1940
2245
|
}
|
|
1941
2246
|
handleNetworkChanged(data) {
|
|
1942
2247
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1943
|
-
// validate networkInfo
|
|
2248
|
+
// validate networkInfo, drop invalid
|
|
1944
2249
|
const networkInfo = validateNetworkInfo(data.networkInfo);
|
|
1945
2250
|
if (!networkInfo) {
|
|
1946
2251
|
this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
|
|
@@ -2001,9 +2306,26 @@
|
|
|
2001
2306
|
}, 2000);
|
|
2002
2307
|
this.logger.log('Transaction confirmed:', data.txHash);
|
|
2003
2308
|
}
|
|
2309
|
+
/**
|
|
2310
|
+
* The wallet takes raw micro-OCT. `amount` passes through unchanged (what every existing
|
|
2311
|
+
* dapp sends today); `amountOct` is converted exactly. Never both.
|
|
2312
|
+
*/
|
|
2313
|
+
resolveRawAmount(amount, amountOct, label) {
|
|
2314
|
+
if (amount != null && amountOct != null) {
|
|
2315
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, `${label}: pass amount or amountOct, not both`);
|
|
2316
|
+
}
|
|
2317
|
+
if (amountOct != null) {
|
|
2318
|
+
this.assertExactOCTAmount(amountOct, label);
|
|
2319
|
+
return octToMicro(amountOct);
|
|
2320
|
+
}
|
|
2321
|
+
if (amount == null || !isValidAmount(amount)) {
|
|
2322
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, `Invalid ${label.toLowerCase()}`);
|
|
2323
|
+
}
|
|
2324
|
+
return amount;
|
|
2325
|
+
}
|
|
2004
2326
|
/**
|
|
2005
2327
|
* Reject numeric amounts that cannot be represented exactly in micro-OCT.
|
|
2006
|
-
* e.g. 0.1 + 0.2 = 0.30000000000000004
|
|
2328
|
+
* e.g. 0.1 + 0.2 = 0.30000000000000004: the extension would sign the wrong value.
|
|
2007
2329
|
* String amounts bypass this check (caller is responsible for correctness).
|
|
2008
2330
|
*/
|
|
2009
2331
|
assertExactOCTAmount(amount, label) {
|
|
@@ -2037,7 +2359,7 @@
|
|
|
2037
2359
|
*
|
|
2038
2360
|
* Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
|
|
2039
2361
|
* window.octra.isOctra === true
|
|
2040
|
-
* window.octra.request({ method, params })
|
|
2362
|
+
* window.octra.request({ method, params }) returns Promise<unknown>
|
|
2041
2363
|
* window.octra.on(event, listener) / removeListener(event, listener)
|
|
2042
2364
|
*
|
|
2043
2365
|
* This adapter translates the SDK's internal method names into RFC-O-1 method
|
|
@@ -2046,7 +2368,7 @@
|
|
|
2046
2368
|
* Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
|
|
2047
2369
|
* are passed through as-is; the wallet's request() handles or rejects them.
|
|
2048
2370
|
*/
|
|
2049
|
-
/** SDK method
|
|
2371
|
+
/** SDK method to RFC-O-1 method name */
|
|
2050
2372
|
const SDK_TO_RFC = {
|
|
2051
2373
|
get_network_info: 'octra_networkInfo',
|
|
2052
2374
|
switch_network: 'octra_switchNetwork',
|
|
@@ -2061,8 +2383,16 @@
|
|
|
2061
2383
|
decrypt_balance: 'octra_decryptBalance',
|
|
2062
2384
|
send_private_transfer: 'octra_sendPrivateTransfer',
|
|
2063
2385
|
claim_private_transfer: 'octra_claimStealth',
|
|
2386
|
+
get_private_capabilities: 'octra_getPrivateCapabilities',
|
|
2387
|
+
encrypt_value: 'octra_encryptValue',
|
|
2388
|
+
decrypt_value: 'octra_decryptValue',
|
|
2389
|
+
make_zero_proof: 'octra_makeZeroProof',
|
|
2390
|
+
make_range_proof: 'octra_makeRangeProof',
|
|
2391
|
+
get_private_balance: 'octra_getPrivateBalance',
|
|
2392
|
+
register_private_view_key: 'octra_registerPrivateViewKey',
|
|
2393
|
+
send_contract_transaction_sequence: 'octra_sendContractTransactionSequence',
|
|
2064
2394
|
};
|
|
2065
|
-
/** RFC-O-1 error code
|
|
2395
|
+
/** RFC-O-1 error code to SDK ErrorCode string */
|
|
2066
2396
|
const RFC_TO_SDK_ERROR = {
|
|
2067
2397
|
4001: 'USER_REJECTED',
|
|
2068
2398
|
4100: 'PERMISSION_DENIED',
|
|
@@ -2164,7 +2494,7 @@
|
|
|
2164
2494
|
const provider = getProvider();
|
|
2165
2495
|
if (!provider)
|
|
2166
2496
|
return () => { _handler = null; };
|
|
2167
|
-
// RFC-O-1 event
|
|
2497
|
+
// RFC-O-1 event to SDK event name and data shape
|
|
2168
2498
|
const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
|
|
2169
2499
|
const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
|
|
2170
2500
|
const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
|
|
@@ -2206,15 +2536,15 @@
|
|
|
2206
2536
|
const OctraProviderAdapter = createOctraProviderAdapter();
|
|
2207
2537
|
|
|
2208
2538
|
/**
|
|
2209
|
-
* 0xio SDK
|
|
2539
|
+
* 0xio SDK: Wallet Adapter Registry
|
|
2210
2540
|
*
|
|
2211
2541
|
* Add new wallet adapters here. Detection order determines which wallet takes
|
|
2212
2542
|
* priority when multiple wallets are installed at the same time.
|
|
2213
2543
|
*/
|
|
2214
2544
|
const REGISTERED_ADAPTERS = [
|
|
2215
|
-
ZeroXIOAdapter, // 0xio extension (postMessage protocol)
|
|
2545
|
+
ZeroXIOAdapter, // 0xio extension (postMessage protocol), highest priority
|
|
2216
2546
|
OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
|
|
2217
|
-
// Add new wallet adapters here
|
|
2547
|
+
// Add new wallet adapters here: detection runs in order, first match wins
|
|
2218
2548
|
];
|
|
2219
2549
|
/**
|
|
2220
2550
|
* Auto-detects the first available wallet in the current page.
|
|
@@ -2261,7 +2591,7 @@
|
|
|
2261
2591
|
*/
|
|
2262
2592
|
// Main exports
|
|
2263
2593
|
// Version information
|
|
2264
|
-
const SDK_VERSION = '2.
|
|
2594
|
+
const SDK_VERSION = '2.8.0';
|
|
2265
2595
|
const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
|
|
2266
2596
|
const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
|
|
2267
2597
|
const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
|
|
@@ -2289,7 +2619,7 @@
|
|
|
2289
2619
|
function checkSDKCompatibility() {
|
|
2290
2620
|
const issues = [];
|
|
2291
2621
|
const recommendations = [];
|
|
2292
|
-
// Hard blockers
|
|
2622
|
+
// Hard blockers: the SDK cannot function without these
|
|
2293
2623
|
if (typeof window === 'undefined') {
|
|
2294
2624
|
issues.push('Window object not available');
|
|
2295
2625
|
recommendations.push('SDK must be used in a browser environment');
|
|
@@ -2354,7 +2684,7 @@
|
|
|
2354
2684
|
debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
|
|
2355
2685
|
environment: isDevelopment ? 'development' : 'production'
|
|
2356
2686
|
}),
|
|
2357
|
-
// simulateExtensionEvent removed for security
|
|
2687
|
+
// simulateExtensionEvent removed for security: it could be exploited on staging builds
|
|
2358
2688
|
showWelcome: () => {
|
|
2359
2689
|
console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
|
|
2360
2690
|
console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
|
|
@@ -2372,16 +2702,21 @@
|
|
|
2372
2702
|
exports.DEFAULT_NETWORK_ID = DEFAULT_NETWORK_ID;
|
|
2373
2703
|
exports.EventEmitter = EventEmitter;
|
|
2374
2704
|
exports.ExtensionCommunicator = ExtensionCommunicator;
|
|
2705
|
+
exports.LEGACY_PERMISSION_MAP = LEGACY_PERMISSION_MAP;
|
|
2375
2706
|
exports.MIN_EXTENSION_VERSION = MIN_EXTENSION_VERSION;
|
|
2376
2707
|
exports.MIN_EXTENSION_VERSION_DEVNET = MIN_EXTENSION_VERSION_DEVNET;
|
|
2377
2708
|
exports.NETWORKS = NETWORKS;
|
|
2378
2709
|
exports.OctraProviderAdapter = OctraProviderAdapter;
|
|
2379
2710
|
exports.SDK_CONFIG = SDK_CONFIG;
|
|
2380
2711
|
exports.SDK_VERSION = SDK_VERSION;
|
|
2712
|
+
exports.SIGNED_MESSAGE_PREFIX = SIGNED_MESSAGE_PREFIX;
|
|
2713
|
+
exports.SIGNED_MESSAGE_VERSION = SIGNED_MESSAGE_VERSION;
|
|
2381
2714
|
exports.SUPPORTED_EXTENSION_VERSIONS = SUPPORTED_EXTENSION_VERSIONS;
|
|
2715
|
+
exports.WALLET_PERMISSIONS = WALLET_PERMISSIONS;
|
|
2382
2716
|
exports.ZeroXIOAdapter = ZeroXIOAdapter;
|
|
2383
2717
|
exports.ZeroXIOWallet = ZeroXIOWallet;
|
|
2384
2718
|
exports.ZeroXIOWalletError = ZeroXIOWalletError;
|
|
2719
|
+
exports.buildAuthMessage = buildAuthMessage;
|
|
2385
2720
|
exports.checkBrowserSupport = checkBrowserSupport;
|
|
2386
2721
|
exports.checkSDKCompatibility = checkSDKCompatibility;
|
|
2387
2722
|
exports.createDefaultBalance = createDefaultBalance;
|
|
@@ -2405,6 +2740,7 @@
|
|
|
2405
2740
|
exports.getAllNetworks = getAllNetworks;
|
|
2406
2741
|
exports.getDefaultNetwork = getDefaultNetwork;
|
|
2407
2742
|
exports.getNetworkConfig = getNetworkConfig;
|
|
2743
|
+
exports.getSignedMessageBytes = getSignedMessageBytes;
|
|
2408
2744
|
exports.isBrowser = isBrowser;
|
|
2409
2745
|
exports.isErrorType = isErrorType;
|
|
2410
2746
|
exports.isValidAddress = isValidAddress;
|
|
@@ -2412,8 +2748,12 @@
|
|
|
2412
2748
|
exports.isValidFeeLevel = isValidFeeLevel;
|
|
2413
2749
|
exports.isValidMessage = isValidMessage;
|
|
2414
2750
|
exports.isValidNetworkId = isValidNetworkId;
|
|
2751
|
+
exports.octToMicro = octToMicro;
|
|
2415
2752
|
exports.toMicroOCT = toMicroOCT;
|
|
2416
2753
|
exports.toMicroZeroXIO = toMicroOCT;
|
|
2754
|
+
exports.toWalletPermissions = toWalletPermissions;
|
|
2755
|
+
exports.verifyMessage = verifyMessage;
|
|
2756
|
+
exports.withLegacyAliases = withLegacyAliases;
|
|
2417
2757
|
|
|
2418
2758
|
}));
|
|
2419
2759
|
//# sourceMappingURL=index.umd.js.map
|