@0xio/sdk 2.7.1 → 2.8.1
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 +80 -39
- package/README.md +104 -280
- package/dist/index.d.ts +254 -45
- package/dist/index.esm.js +410 -79
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +419 -78
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +419 -78
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -94,6 +94,16 @@ exports.ErrorCode = void 0;
|
|
|
94
94
|
ErrorCode["DUPLICATE_TRANSACTION"] = "DUPLICATE_TRANSACTION";
|
|
95
95
|
ErrorCode["NONCE_TOO_FAR"] = "NONCE_TOO_FAR";
|
|
96
96
|
ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
97
|
+
// Codes the 0xio wallet returns over the bridge
|
|
98
|
+
ErrorCode["NOT_CONNECTED"] = "NOT_CONNECTED";
|
|
99
|
+
ErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS";
|
|
100
|
+
ErrorCode["METHOD_NOT_ALLOWED"] = "METHOD_NOT_ALLOWED";
|
|
101
|
+
ErrorCode["NOT_AVAILABLE"] = "NOT_AVAILABLE";
|
|
102
|
+
ErrorCode["PRIVATE_PROOF_FAILED"] = "PRIVATE_PROOF_FAILED";
|
|
103
|
+
ErrorCode["PRIVATE_TRANSFER_FAILED"] = "PRIVATE_TRANSFER_FAILED";
|
|
104
|
+
ErrorCode["CONTRACT_CALL_FAILED"] = "CONTRACT_CALL_FAILED";
|
|
105
|
+
ErrorCode["SIGN_FAILED"] = "SIGN_FAILED";
|
|
106
|
+
ErrorCode["RECIPIENT_NOT_REGISTERED"] = "RECIPIENT_NOT_REGISTERED";
|
|
97
107
|
})(exports.ErrorCode || (exports.ErrorCode = {}));
|
|
98
108
|
class ZeroXIOWalletError extends Error {
|
|
99
109
|
constructor(code, message, details) {
|
|
@@ -116,7 +126,7 @@ class ZeroXIOWalletError extends Error {
|
|
|
116
126
|
* window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
|
|
117
127
|
* window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
|
|
118
128
|
*
|
|
119
|
-
*
|
|
129
|
+
* Session nonce validation: injected.ts broadcasts the nonce received from the
|
|
120
130
|
* isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
|
|
121
131
|
* mismatched nonce is rejected, preventing response injection by malicious page scripts.
|
|
122
132
|
*/
|
|
@@ -149,7 +159,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
|
149
159
|
window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
|
|
150
160
|
}
|
|
151
161
|
catch {
|
|
152
|
-
// Do not fall back to '*'
|
|
162
|
+
// Do not fall back to '*': silent failure is safer
|
|
153
163
|
}
|
|
154
164
|
},
|
|
155
165
|
listen(handler, options) {
|
|
@@ -164,7 +174,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
|
164
174
|
...(options?.trustedParentOrigins ?? []),
|
|
165
175
|
]);
|
|
166
176
|
let _sessionNonce = null;
|
|
167
|
-
//
|
|
177
|
+
// receive the session nonce from injected.ts (MAIN world content script)
|
|
168
178
|
const nonceListener = (e) => {
|
|
169
179
|
if (e.origin !== allowedOrigin)
|
|
170
180
|
return;
|
|
@@ -187,7 +197,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
|
187
197
|
return;
|
|
188
198
|
if (!e.data || e.data.source !== '0xio-sdk-bridge')
|
|
189
199
|
return;
|
|
190
|
-
//
|
|
200
|
+
// session nonce validation.
|
|
191
201
|
// Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
|
|
192
202
|
// Fallback path: if the init broadcast was missed (race between document_start
|
|
193
203
|
// content script and page script load), capture nonce from the first same-origin
|
|
@@ -257,7 +267,7 @@ function isValidAmount(amount) {
|
|
|
257
267
|
amount <= Number.MAX_SAFE_INTEGER;
|
|
258
268
|
}
|
|
259
269
|
function isValidMessage(message) {
|
|
260
|
-
// Type check first
|
|
270
|
+
// Type check first: falsy non-strings (0, false, null) are not valid messages
|
|
261
271
|
if (typeof message !== 'string') {
|
|
262
272
|
return message === undefined || message === null ? true : false;
|
|
263
273
|
}
|
|
@@ -265,7 +275,7 @@ function isValidMessage(message) {
|
|
|
265
275
|
if (message.length === 0) {
|
|
266
276
|
return true;
|
|
267
277
|
}
|
|
268
|
-
// 100KB limit
|
|
278
|
+
// 100KB limit: contract call params can be large (serialized JSON)
|
|
269
279
|
return message.length <= 100000;
|
|
270
280
|
}
|
|
271
281
|
function isValidFeeLevel(feeLevel) {
|
|
@@ -298,7 +308,7 @@ function _base58Encode(buf) {
|
|
|
298
308
|
}
|
|
299
309
|
/**
|
|
300
310
|
* Derive the canonical Octra address from a base64-encoded Ed25519 public key.
|
|
301
|
-
* Algorithm: SHA-256
|
|
311
|
+
* Algorithm: SHA-256 of the pubkey bytes, base58-encoded, with "oct" prepended
|
|
302
312
|
* Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
|
|
303
313
|
*/
|
|
304
314
|
async function deriveOctraAddress(publicKeyBase64) {
|
|
@@ -357,6 +367,22 @@ function toMicroOCT(amount) {
|
|
|
357
367
|
const microOCT = Math.round(amount * 1000000);
|
|
358
368
|
return microOCT.toString();
|
|
359
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Exact OCT to raw micro-OCT conversion using string arithmetic (no float rounding).
|
|
372
|
+
* Accepts up to 6 decimals; anything else is rejected.
|
|
373
|
+
*/
|
|
374
|
+
function octToMicro(amount) {
|
|
375
|
+
const text = typeof amount === 'number' ? amount.toFixed(6) : String(amount).trim();
|
|
376
|
+
if (!/^\d+(\.\d{1,6})?$/.test(text)) {
|
|
377
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Amount must be a positive decimal with at most 6 places');
|
|
378
|
+
}
|
|
379
|
+
const [whole, frac = ''] = text.split('.');
|
|
380
|
+
const micro = BigInt(whole) * BigInt(1000000) + BigInt(frac.padEnd(6, '0'));
|
|
381
|
+
if (micro <= BigInt(0)) {
|
|
382
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Amount must be greater than zero');
|
|
383
|
+
}
|
|
384
|
+
return micro.toString();
|
|
385
|
+
}
|
|
360
386
|
function fromMicroOCT(microAmount) {
|
|
361
387
|
const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
|
|
362
388
|
if (!Number.isFinite(amount) || amount < 0) {
|
|
@@ -385,7 +411,16 @@ function createErrorMessage(code, context) {
|
|
|
385
411
|
[exports.ErrorCode.INVALID_SIGNATURE]: 'Invalid transaction signature',
|
|
386
412
|
[exports.ErrorCode.DUPLICATE_TRANSACTION]: 'Duplicate transaction detected',
|
|
387
413
|
[exports.ErrorCode.NONCE_TOO_FAR]: 'Transaction nonce is too far ahead',
|
|
388
|
-
[exports.ErrorCode.INTERNAL_ERROR]: 'Internal server error'
|
|
414
|
+
[exports.ErrorCode.INTERNAL_ERROR]: 'Internal server error',
|
|
415
|
+
[exports.ErrorCode.NOT_CONNECTED]: 'Connect the wallet before this request',
|
|
416
|
+
[exports.ErrorCode.INVALID_PARAMS]: 'Invalid request parameters',
|
|
417
|
+
[exports.ErrorCode.METHOD_NOT_ALLOWED]: 'This method is not permitted through the wallet',
|
|
418
|
+
[exports.ErrorCode.NOT_AVAILABLE]: 'Not available through the wallet bridge',
|
|
419
|
+
[exports.ErrorCode.PRIVATE_PROOF_FAILED]: 'Private proof generation failed',
|
|
420
|
+
[exports.ErrorCode.PRIVATE_TRANSFER_FAILED]: 'Private transfer failed',
|
|
421
|
+
[exports.ErrorCode.CONTRACT_CALL_FAILED]: 'Contract call failed',
|
|
422
|
+
[exports.ErrorCode.SIGN_FAILED]: 'Signing failed',
|
|
423
|
+
[exports.ErrorCode.RECIPIENT_NOT_REGISTERED]: 'Recipient has no private view key registered'
|
|
389
424
|
};
|
|
390
425
|
const baseMessage = baseMessages[code] || 'Unknown error';
|
|
391
426
|
return context ? `${baseMessage}: ${context}` : baseMessage;
|
|
@@ -406,7 +441,7 @@ async function retry(operation, maxRetries = 3, baseDelay = 1000) {
|
|
|
406
441
|
}
|
|
407
442
|
catch (error) {
|
|
408
443
|
lastError = error;
|
|
409
|
-
// Never retry user rejections
|
|
444
|
+
// Never retry user rejections, they are intentional
|
|
410
445
|
const msg = lastError.message?.toLowerCase() || '';
|
|
411
446
|
if (msg.includes('rejected') || msg.includes('denied') || msg.includes('cancelled') || msg.includes('user refused')) {
|
|
412
447
|
throw lastError;
|
|
@@ -548,14 +583,14 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
548
583
|
this._adapterReadyTeardown = null;
|
|
549
584
|
/**
|
|
550
585
|
* Set when a trusted walletReady has been received from window.parent.
|
|
551
|
-
* The polling fallback must
|
|
586
|
+
* The polling fallback must not clear this flag.
|
|
552
587
|
*/
|
|
553
588
|
this._parentTrusted = false;
|
|
554
589
|
/** walletReady postMessage listener stored for cleanup */
|
|
555
590
|
this._walletReadyMessageListener = null;
|
|
556
591
|
/**
|
|
557
592
|
* In-flight interactive request lock.
|
|
558
|
-
* Methods that open approval popups are serialized
|
|
593
|
+
* Methods that open approval popups are serialized: only one at a time.
|
|
559
594
|
*/
|
|
560
595
|
this._interactiveInFlight = false;
|
|
561
596
|
this.MAX_CONCURRENT_REQUESTS = 50;
|
|
@@ -596,9 +631,15 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
596
631
|
return this.isExtensionAvailableState && this.hasExtensionContext();
|
|
597
632
|
}
|
|
598
633
|
async sendRequest(method, params = {}, timeout = 30000) {
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
634
|
+
// No-retry + long timeout for both popup/broadcast methods and long compute primitives.
|
|
635
|
+
const longOrNoRetry = ExtensionCommunicator.NO_RETRY_METHODS.has(method) ||
|
|
636
|
+
ExtensionCommunicator.LONG_COMPUTE_METHODS.has(method);
|
|
637
|
+
const maxRetries = longOrNoRetry ? 0 : 1;
|
|
638
|
+
const effectiveTimeout = ExtensionCommunicator.PROOF_METHODS.has(method)
|
|
639
|
+
? Math.max(timeout, 600000)
|
|
640
|
+
: longOrNoRetry
|
|
641
|
+
? Math.max(timeout, 180000)
|
|
642
|
+
: timeout;
|
|
602
643
|
return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
|
|
603
644
|
}
|
|
604
645
|
async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
|
|
@@ -667,7 +708,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
667
708
|
return;
|
|
668
709
|
this._adapterTeardown = this.adapter.listen((msg) => {
|
|
669
710
|
if (msg.requestId !== undefined) {
|
|
670
|
-
// response
|
|
711
|
+
// response: map AdapterIncomingMessage to the ExtensionResponse shape
|
|
671
712
|
if (this.pendingRequests.has(msg.requestId)) {
|
|
672
713
|
this.handleExtensionResponse({
|
|
673
714
|
id: msg.requestId,
|
|
@@ -701,7 +742,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
701
742
|
}
|
|
702
743
|
clearTimeout(pending.timeout);
|
|
703
744
|
this.pendingRequests.delete(response.id);
|
|
704
|
-
// Require strict boolean true
|
|
745
|
+
// Require strict boolean true: a "false" string or other truthy values are failures
|
|
705
746
|
if (response.success === true) {
|
|
706
747
|
pending.resolve(response.data);
|
|
707
748
|
}
|
|
@@ -726,7 +767,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
726
767
|
}
|
|
727
768
|
postMessageToExtension(request) {
|
|
728
769
|
this.adapter.postRequest(request);
|
|
729
|
-
// Parent bridge (iframe/desktop mode)
|
|
770
|
+
// Parent bridge (iframe/desktop mode), only when a trusted origin is established.
|
|
730
771
|
// Sending with '*' would leak method + params to any intercepting frame.
|
|
731
772
|
if (window.parent !== window && this._parentOrigin) {
|
|
732
773
|
if (this.adapter.postRequestToParent) {
|
|
@@ -743,7 +784,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
743
784
|
if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
|
|
744
785
|
throw new ZeroXIOWalletError(exports.ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
|
|
745
786
|
}
|
|
746
|
-
// Trim expired timestamps
|
|
787
|
+
// Trim expired timestamps and cap the array size to prevent unbounded growth in idle tabs
|
|
747
788
|
this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
|
|
748
789
|
if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
|
|
749
790
|
this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
|
|
@@ -763,7 +804,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
763
804
|
const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
|
764
805
|
return `0xio-sdk-${hex}`;
|
|
765
806
|
}
|
|
766
|
-
// Crypto API unavailable
|
|
807
|
+
// Crypto API unavailable: throw rather than produce a guessable ID that
|
|
767
808
|
// could allow response spoofing via a known requestId.
|
|
768
809
|
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
|
|
769
810
|
}
|
|
@@ -777,7 +818,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
777
818
|
this.isExtensionAvailableState = true;
|
|
778
819
|
});
|
|
779
820
|
}
|
|
780
|
-
// walletReady via postMessage (desktop/mobile iframe bridge)
|
|
821
|
+
// walletReady via postMessage (desktop/mobile iframe bridge), store the ref for cleanup
|
|
781
822
|
this._walletReadyMessageListener = (event) => {
|
|
782
823
|
if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
|
|
783
824
|
return;
|
|
@@ -812,7 +853,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
812
853
|
};
|
|
813
854
|
window.addEventListener('message', this._walletReadyMessageListener);
|
|
814
855
|
if (window.parent !== window) {
|
|
815
|
-
this.logger.log('Running inside a frame
|
|
856
|
+
this.logger.log('Running inside a frame, waiting for the trusted walletReady signal');
|
|
816
857
|
}
|
|
817
858
|
this.checkExtensionAvailability();
|
|
818
859
|
this.extensionDetectionInterval = setInterval(() => {
|
|
@@ -821,7 +862,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
821
862
|
}
|
|
822
863
|
checkExtensionAvailability() {
|
|
823
864
|
// If parent-bridge readiness was established via a trusted walletReady handshake,
|
|
824
|
-
// preserve that state
|
|
865
|
+
// preserve that state: the polling fallback (detectExtensionSignals) does not
|
|
825
866
|
// consider the iframe parent signal and would incorrectly flip state back
|
|
826
867
|
if (this._parentTrusted) {
|
|
827
868
|
return;
|
|
@@ -903,7 +944,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
903
944
|
}
|
|
904
945
|
/**
|
|
905
946
|
* Clean up SDK resources.
|
|
906
|
-
* After cleanup() the instance is terminal
|
|
947
|
+
* After cleanup() the instance is terminal: do not call initialize() again.
|
|
907
948
|
* Construct a new instance instead.
|
|
908
949
|
*/
|
|
909
950
|
cleanup() {
|
|
@@ -944,7 +985,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
944
985
|
};
|
|
945
986
|
}
|
|
946
987
|
}
|
|
947
|
-
// Methods that trigger user-facing popups
|
|
988
|
+
// Methods that trigger user-facing popups: never retry these.
|
|
948
989
|
// Retrying sends a second request while the first popup is still open,
|
|
949
990
|
// causing double popups where the second tx fails (stale nonce/state).
|
|
950
991
|
ExtensionCommunicator.NO_RETRY_METHODS = new Set([
|
|
@@ -952,12 +993,29 @@ ExtensionCommunicator.NO_RETRY_METHODS = new Set([
|
|
|
952
993
|
'sign_transaction', 'broadcast_only',
|
|
953
994
|
'send_private_transfer', 'claim_private_transfer',
|
|
954
995
|
'encrypt_balance', 'decrypt_balance',
|
|
996
|
+
// Broadcasts a sequence of contract txs, so it must never be retried (it would double-send).
|
|
997
|
+
'send_contract_transaction_sequence',
|
|
998
|
+
// Shows an approval popup + broadcasts a registration tx (offscreen cold-init can exceed
|
|
999
|
+
// 30s): interactive, long timeout, no retry.
|
|
1000
|
+
'register_private_view_key',
|
|
1001
|
+
]);
|
|
1002
|
+
// Long-running compute primitives (RFP): proof generation / decrypt take 10-120s, so
|
|
1003
|
+
// they need the long (180s) timeout and must not be retried (a retry wastes ~a minute of
|
|
1004
|
+
// compute). They do not show approval popups, so, unlike NO_RETRY_METHODS, they are not
|
|
1005
|
+
// subject to the one-at-a-time interactive lock (a dapp may run several concurrently).
|
|
1006
|
+
ExtensionCommunicator.LONG_COMPUTE_METHODS = new Set([
|
|
1007
|
+
'make_zero_proof', 'make_range_proof', 'decrypt_value',
|
|
1008
|
+
'get_private_balance', 'encrypt_value',
|
|
955
1009
|
]);
|
|
1010
|
+
// A private transfer runs proof generation after the approval (minutes on a slow machine),
|
|
1011
|
+
// so its wait is longer than the popup window alone.
|
|
1012
|
+
ExtensionCommunicator.PROOF_METHODS = new Set(['send_private_transfer']);
|
|
1013
|
+
// The interactive lock applies only to methods that open a wallet popup / broadcast.
|
|
956
1014
|
ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
|
|
957
1015
|
// only forward known event types
|
|
958
1016
|
ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
|
|
959
1017
|
'connect', 'disconnect', 'accountChanged', 'balanceChanged',
|
|
960
|
-
'networkChanged', 'transactionConfirmed', 'permissionsChanged', 'message',
|
|
1018
|
+
'networkChanged', 'transactionConfirmed', 'transactionFailed', 'permissionsChanged', 'message',
|
|
961
1019
|
'error', 'extensionLocked', 'extensionUnlocked'
|
|
962
1020
|
]);
|
|
963
1021
|
|
|
@@ -979,7 +1037,7 @@ const _NETWORKS = {
|
|
|
979
1037
|
'devnet': {
|
|
980
1038
|
id: 'devnet',
|
|
981
1039
|
name: 'Octra Devnet',
|
|
982
|
-
rpcUrl: '
|
|
1040
|
+
rpcUrl: 'https://devnet.octrascan.io',
|
|
983
1041
|
explorerUrl: 'https://devnet.octrascan.io/tx.html?hash=',
|
|
984
1042
|
explorerAddressUrl: 'https://devnet.octrascan.io/address.html?addr=',
|
|
985
1043
|
indexerUrl: 'https://devnet.octrascan.io',
|
|
@@ -1007,7 +1065,7 @@ const NETWORKS = Object.freeze(Object.fromEntries(Object.entries(_NETWORKS).map(
|
|
|
1007
1065
|
const DEFAULT_NETWORK_ID = 'mainnet';
|
|
1008
1066
|
/**
|
|
1009
1067
|
* Get network configuration by ID.
|
|
1010
|
-
* Returns a frozen copy
|
|
1068
|
+
* Returns a frozen copy, so callers cannot mutate SDK-internal state.
|
|
1011
1069
|
*/
|
|
1012
1070
|
function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
|
|
1013
1071
|
if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
|
|
@@ -1017,7 +1075,7 @@ function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
|
|
|
1017
1075
|
}
|
|
1018
1076
|
/**
|
|
1019
1077
|
* Get all available networks.
|
|
1020
|
-
* Returns frozen copies
|
|
1078
|
+
* Returns frozen copies, so callers cannot mutate SDK-internal state.
|
|
1021
1079
|
*/
|
|
1022
1080
|
function getAllNetworks() {
|
|
1023
1081
|
return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
|
|
@@ -1066,7 +1124,7 @@ function validateNetworkInfo(raw) {
|
|
|
1066
1124
|
|
|
1067
1125
|
/**
|
|
1068
1126
|
* Default balance structure.
|
|
1069
|
-
* Accepts a numeric total or undefined
|
|
1127
|
+
* Accepts a numeric total or undefined. Never pass a Balance object here.
|
|
1070
1128
|
*/
|
|
1071
1129
|
function createDefaultBalance(total = 0) {
|
|
1072
1130
|
const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
|
|
@@ -1085,7 +1143,7 @@ function validateBalance(raw) {
|
|
|
1085
1143
|
if (raw === null || raw === undefined)
|
|
1086
1144
|
return null;
|
|
1087
1145
|
// If it's already a Balance-shaped object, extract numeric fields
|
|
1088
|
-
// Use Number() not parseFloat()
|
|
1146
|
+
// Use Number() not parseFloat(): parseFloat('10abc') silently returns 10
|
|
1089
1147
|
const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
|
|
1090
1148
|
const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
|
|
1091
1149
|
if (!Number.isFinite(pub) || pub < 0)
|
|
@@ -1100,7 +1158,7 @@ function validateBalance(raw) {
|
|
|
1100
1158
|
};
|
|
1101
1159
|
}
|
|
1102
1160
|
const SDK_CONFIG = {
|
|
1103
|
-
version: '2.
|
|
1161
|
+
version: '2.8.0',
|
|
1104
1162
|
defaultNetworkId: DEFAULT_NETWORK_ID,
|
|
1105
1163
|
communicationTimeout: 30000, // 30 seconds
|
|
1106
1164
|
retryAttempts: 3,
|
|
@@ -1110,13 +1168,154 @@ function getDefaultNetwork() {
|
|
|
1110
1168
|
return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
|
|
1111
1169
|
}
|
|
1112
1170
|
|
|
1171
|
+
/** Scope names the 0xio wallet enforces. Any other name is dropped at connect. */
|
|
1172
|
+
const WALLET_PERMISSIONS = [
|
|
1173
|
+
'accounts',
|
|
1174
|
+
'public_transactions',
|
|
1175
|
+
'contract_calls',
|
|
1176
|
+
'contract_views',
|
|
1177
|
+
'private_balance_read',
|
|
1178
|
+
'private_proofs',
|
|
1179
|
+
'private_transfers',
|
|
1180
|
+
'private_claims',
|
|
1181
|
+
];
|
|
1182
|
+
/** Older SDK permission names and the wallet scope each one means. */
|
|
1183
|
+
const LEGACY_PERMISSION_MAP = {
|
|
1184
|
+
read_address: 'accounts',
|
|
1185
|
+
read_balance: 'accounts',
|
|
1186
|
+
read_public_key: 'accounts',
|
|
1187
|
+
send_transactions: 'public_transactions',
|
|
1188
|
+
sign_messages: 'public_transactions',
|
|
1189
|
+
contract_calls: 'contract_calls',
|
|
1190
|
+
view_private_balance: 'private_balance_read',
|
|
1191
|
+
view_encrypted_balance: 'private_balance_read',
|
|
1192
|
+
stealth_scan: 'private_balance_read',
|
|
1193
|
+
decrypt_balance: 'private_balance_read',
|
|
1194
|
+
encrypt_balance: 'private_proofs',
|
|
1195
|
+
private_transfers: 'private_transfers',
|
|
1196
|
+
stealth_claim: 'private_claims',
|
|
1197
|
+
};
|
|
1198
|
+
/** Translate any mix of old and new names into the wallet's scope names, without duplicates. */
|
|
1199
|
+
function toWalletPermissions(perms) {
|
|
1200
|
+
const out = [];
|
|
1201
|
+
for (const p of perms ?? []) {
|
|
1202
|
+
const canonical = WALLET_PERMISSIONS.includes(p)
|
|
1203
|
+
? p
|
|
1204
|
+
: LEGACY_PERMISSION_MAP[p];
|
|
1205
|
+
if (canonical && !out.includes(canonical))
|
|
1206
|
+
out.push(canonical);
|
|
1207
|
+
}
|
|
1208
|
+
return out;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* The granted wallet scopes plus every requested old name they satisfy, so a dapp that checks
|
|
1212
|
+
* for the name it asked for (for example 'read_balance') keeps seeing it.
|
|
1213
|
+
*/
|
|
1214
|
+
function withLegacyAliases(granted, requested) {
|
|
1215
|
+
const set = new Set(granted ?? []);
|
|
1216
|
+
for (const p of requested ?? []) {
|
|
1217
|
+
const canonical = LEGACY_PERMISSION_MAP[p];
|
|
1218
|
+
if (canonical && set.has(canonical))
|
|
1219
|
+
set.add(p);
|
|
1220
|
+
}
|
|
1221
|
+
return [...set];
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* 0xio Signed Message standard (v1).
|
|
1226
|
+
*
|
|
1227
|
+
* `wallet.signMessage(message)` never signs the raw message. The wallet frames it first so a signed
|
|
1228
|
+
* "message" can never collide with a transaction pre-image (a transaction is canonical JSON that
|
|
1229
|
+
* begins with '{'). The framing is:
|
|
1230
|
+
*
|
|
1231
|
+
* "Octra Signed Message:\n" + utf8ByteLength(message) + "\n" + message
|
|
1232
|
+
*
|
|
1233
|
+
* signed as an Ed25519 detached signature over the UTF-8 bytes of that string. The leading 'O'
|
|
1234
|
+
* guarantees the signed bytes never begin with '{', so a personal-message signature can never be a
|
|
1235
|
+
* valid transaction. Any verifier MUST reconstruct the same bytes: use `getSignedMessageBytes()`
|
|
1236
|
+
* with any Ed25519 library, or `verifyMessage()` for a batteries-included check.
|
|
1237
|
+
*/
|
|
1238
|
+
/** Fixed prefix tag for the 0xio Signed Message scheme. */
|
|
1239
|
+
const SIGNED_MESSAGE_PREFIX = 'Octra Signed Message:';
|
|
1240
|
+
/** Scheme version, bumped if the framing ever changes so verifiers can detect it. */
|
|
1241
|
+
const SIGNED_MESSAGE_VERSION = 1;
|
|
1242
|
+
/** UTF-8 byte length of a string (JS `.length` counts UTF-16 units, not bytes). */
|
|
1243
|
+
function utf8ByteLength(s) {
|
|
1244
|
+
return new TextEncoder().encode(s).length;
|
|
1245
|
+
}
|
|
1246
|
+
function base64ToBytes(b64) {
|
|
1247
|
+
// atob is available in browsers and Node 16+; utils.deriveOctraAddress uses the same path.
|
|
1248
|
+
const bin = atob(b64);
|
|
1249
|
+
const out = new Uint8Array(bin.length);
|
|
1250
|
+
for (let i = 0; i < bin.length; i++)
|
|
1251
|
+
out[i] = bin.charCodeAt(i);
|
|
1252
|
+
return out;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* The exact bytes that `wallet.signMessage(message)` produces a signature over. Verify a 0xio
|
|
1256
|
+
* message signature by checking an Ed25519 signature against these bytes with the signer's public
|
|
1257
|
+
* key. Zero-dependency - bring your own Ed25519 verifier, or use `verifyMessage`.
|
|
1258
|
+
*/
|
|
1259
|
+
function getSignedMessageBytes(message) {
|
|
1260
|
+
if (typeof message !== 'string') {
|
|
1261
|
+
throw new TypeError('message must be a string');
|
|
1262
|
+
}
|
|
1263
|
+
const framed = `${SIGNED_MESSAGE_PREFIX}\n${utf8ByteLength(message)}\n${message}`;
|
|
1264
|
+
return new TextEncoder().encode(framed);
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Reconstruct the auth message that `wallet.signAuthMessage(service, nonce)` signs. A relying
|
|
1268
|
+
* service verifies an auth signature with `verifyMessage(buildAuthMessage(service, nonce, origin),
|
|
1269
|
+
* signature, publicKey)`, where `origin` is the caller's page origin.
|
|
1270
|
+
*/
|
|
1271
|
+
function buildAuthMessage(service, nonce, origin) {
|
|
1272
|
+
return `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Verify a 0xio message signature produced by `wallet.signMessage`.
|
|
1276
|
+
*
|
|
1277
|
+
* @param message The original message passed to `wallet.signMessage`.
|
|
1278
|
+
* @param signature Base64 Ed25519 signature returned by `wallet.signMessage`.
|
|
1279
|
+
* @param publicKey Base64 Ed25519 public key of the signer (from `wallet.getPublicKey()`).
|
|
1280
|
+
* @returns Whether the signature is valid for this message and key.
|
|
1281
|
+
*
|
|
1282
|
+
* Uses the Web Crypto Ed25519 primitive (Node 18+, Chrome 137+, Safari 17+, Firefox 129+). In an
|
|
1283
|
+
* environment without it, verify `getSignedMessageBytes(message)` with your own Ed25519 library.
|
|
1284
|
+
*/
|
|
1285
|
+
async function verifyMessage(message, signature, publicKey) {
|
|
1286
|
+
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
1287
|
+
throw new Error('Web Crypto API unavailable; verify getSignedMessageBytes(message) with your own Ed25519 library');
|
|
1288
|
+
}
|
|
1289
|
+
let sigBytes;
|
|
1290
|
+
let pubBytes;
|
|
1291
|
+
try {
|
|
1292
|
+
sigBytes = base64ToBytes(signature);
|
|
1293
|
+
pubBytes = base64ToBytes(publicKey);
|
|
1294
|
+
}
|
|
1295
|
+
catch {
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
try {
|
|
1299
|
+
const data = new Uint8Array(getSignedMessageBytes(message));
|
|
1300
|
+
const key = await crypto.subtle.importKey('raw', pubBytes, { name: 'Ed25519' }, false, [
|
|
1301
|
+
'verify',
|
|
1302
|
+
]);
|
|
1303
|
+
return await crypto.subtle.verify({ name: 'Ed25519' }, key, sigBytes, data);
|
|
1304
|
+
}
|
|
1305
|
+
catch {
|
|
1306
|
+
// importKey/verify can throw (rather than return false) on malformed input or an environment
|
|
1307
|
+
// that recognizes Ed25519 only partially; treat any such failure as "not verified".
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1113
1312
|
class ZeroXIOWallet extends EventEmitter {
|
|
1114
1313
|
constructor(config) {
|
|
1115
1314
|
super(config.debug);
|
|
1116
1315
|
this.connectionInfo = { isConnected: false };
|
|
1117
1316
|
this.isInitialized = false;
|
|
1118
1317
|
this._initPromise = null;
|
|
1119
|
-
// session version
|
|
1318
|
+
// session version, for stale write detection
|
|
1120
1319
|
this._sessionVersion = 0;
|
|
1121
1320
|
this.config = {
|
|
1122
1321
|
...config,
|
|
@@ -1147,7 +1346,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1147
1346
|
appVersion: this.config.appVersion,
|
|
1148
1347
|
appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
1149
1348
|
appIcon: this.config.appIcon,
|
|
1150
|
-
requiredPermissions: this.config.requiredPermissions,
|
|
1349
|
+
requiredPermissions: toWalletPermissions(this.config.requiredPermissions),
|
|
1151
1350
|
networkId: this.config.networkId
|
|
1152
1351
|
});
|
|
1153
1352
|
this.setupExtensionEventListeners();
|
|
@@ -1175,22 +1374,22 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1175
1374
|
this.ensureInitialized();
|
|
1176
1375
|
try {
|
|
1177
1376
|
this.logger.log('Attempting to connect with options:', options);
|
|
1178
|
-
// filter to declared perms only
|
|
1377
|
+
// filter to declared perms only: accept both RFC 'permissions' and legacy 'requestPermissions'
|
|
1179
1378
|
const declaredPermissions = this.config.requiredPermissions || [];
|
|
1180
1379
|
const requestedPerms = options.permissions ?? options.requestPermissions;
|
|
1181
1380
|
const requestedPermissions = requestedPerms
|
|
1182
1381
|
? requestedPerms.filter(p => declaredPermissions.includes(p))
|
|
1183
1382
|
: declaredPermissions;
|
|
1184
1383
|
const result = await this.communicator.sendRequest('connect', {
|
|
1185
|
-
permissions: requestedPermissions,
|
|
1384
|
+
permissions: toWalletPermissions(requestedPermissions),
|
|
1186
1385
|
networkId: options.networkId || this.config.networkId
|
|
1187
1386
|
});
|
|
1188
|
-
// verify
|
|
1387
|
+
// verify the public key to address binding
|
|
1189
1388
|
if (result.publicKey && result.address) {
|
|
1190
1389
|
try {
|
|
1191
1390
|
const derived = await deriveOctraAddress(result.publicKey);
|
|
1192
1391
|
if (derived !== result.address) {
|
|
1193
|
-
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed
|
|
1392
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed: the reported public key does not derive to the reported address');
|
|
1194
1393
|
}
|
|
1195
1394
|
}
|
|
1196
1395
|
catch (e) {
|
|
@@ -1199,14 +1398,16 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1199
1398
|
this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
|
|
1200
1399
|
}
|
|
1201
1400
|
}
|
|
1202
|
-
// Use networkInfo from extension response
|
|
1401
|
+
// Use networkInfo from the extension response, validated before caching.
|
|
1203
1402
|
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1204
1403
|
?? (result.networkId ? getNetworkConfig(result.networkId) : null);
|
|
1205
1404
|
if (!networkInfo) {
|
|
1206
1405
|
throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
|
|
1207
1406
|
}
|
|
1208
|
-
|
|
1209
|
-
//
|
|
1407
|
+
// The wallet answers with its own scope names; the names the dapp asked for are kept as
|
|
1408
|
+
// aliases so existing permission checks keep working.
|
|
1409
|
+
const permissions = withLegacyAliases(result.permissions, requestedPermissions);
|
|
1410
|
+
// Update connection info, including permissions
|
|
1210
1411
|
this.connectionInfo = {
|
|
1211
1412
|
isConnected: true,
|
|
1212
1413
|
address: result.address,
|
|
@@ -1280,12 +1481,12 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1280
1481
|
if (this._sessionVersion !== sv)
|
|
1281
1482
|
return { ...this.connectionInfo };
|
|
1282
1483
|
if (result.isConnected && result.address) {
|
|
1283
|
-
// verify
|
|
1484
|
+
// verify the public key to address binding
|
|
1284
1485
|
if (result.publicKey) {
|
|
1285
1486
|
try {
|
|
1286
1487
|
const derived = await deriveOctraAddress(result.publicKey);
|
|
1287
1488
|
if (derived !== result.address) {
|
|
1288
|
-
this.logger.warn('Address-key binding mismatch on session restore
|
|
1489
|
+
this.logger.warn('Address-key binding mismatch on session restore, ignoring stale session');
|
|
1289
1490
|
this.connectionInfo = { isConnected: false };
|
|
1290
1491
|
return { ...this.connectionInfo };
|
|
1291
1492
|
}
|
|
@@ -1299,11 +1500,11 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1299
1500
|
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1300
1501
|
?? (result.networkId ? getNetworkConfig(result.networkId) : null);
|
|
1301
1502
|
if (!networkInfo) {
|
|
1302
|
-
this.logger.warn('getConnectionStatus: wallet returned no network metadata
|
|
1503
|
+
this.logger.warn('getConnectionStatus: wallet returned no network metadata, returning cached state');
|
|
1303
1504
|
return this.connectionInfo;
|
|
1304
1505
|
}
|
|
1305
1506
|
const wasConnected = this.connectionInfo.isConnected;
|
|
1306
|
-
const permissions = result.permissions
|
|
1507
|
+
const permissions = withLegacyAliases(result.permissions, this.config.requiredPermissions);
|
|
1307
1508
|
// preserve existing connectedAt
|
|
1308
1509
|
const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
|
|
1309
1510
|
this.connectionInfo = {
|
|
@@ -1316,7 +1517,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1316
1517
|
permissions
|
|
1317
1518
|
};
|
|
1318
1519
|
this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
|
|
1319
|
-
// only emit on disconnected
|
|
1520
|
+
// only emit on the disconnected to connected transition
|
|
1320
1521
|
if (!wasConnected) {
|
|
1321
1522
|
const connectEvent = {
|
|
1322
1523
|
address: result.address,
|
|
@@ -1341,9 +1542,10 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1341
1542
|
}
|
|
1342
1543
|
}
|
|
1343
1544
|
/**
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1545
|
+
* Ask the wallet to switch its active network (e.g. 'mainnet' to 'devnet').
|
|
1546
|
+
* The wallet asks the user to confirm first; declining rejects with USER_REJECTED. The call is
|
|
1547
|
+
* also refused while another request from this page is waiting for approval.
|
|
1548
|
+
* On a switch the wallet emits 'networkChanged' to connected dApps.
|
|
1347
1549
|
*/
|
|
1348
1550
|
async switchNetwork(networkId) {
|
|
1349
1551
|
this.ensureConnected();
|
|
@@ -1377,6 +1579,22 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1377
1579
|
getAddress() {
|
|
1378
1580
|
return this.connectionInfo.address || null;
|
|
1379
1581
|
}
|
|
1582
|
+
/**
|
|
1583
|
+
* The connected account's Ed25519 public key (base64). Served from the session when the
|
|
1584
|
+
* wallet reported it at connect, otherwise asked from the wallet.
|
|
1585
|
+
*/
|
|
1586
|
+
async getPublicKey() {
|
|
1587
|
+
this.ensureConnected();
|
|
1588
|
+
if (this.connectionInfo.publicKey)
|
|
1589
|
+
return this.connectionInfo.publicKey;
|
|
1590
|
+
const result = await this.communicator.sendRequest('getPublicKey');
|
|
1591
|
+
const publicKey = result?.publicKey;
|
|
1592
|
+
if (typeof publicKey !== 'string' || !publicKey) {
|
|
1593
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Wallet did not return a public key');
|
|
1594
|
+
}
|
|
1595
|
+
this.connectionInfo.publicKey = publicKey;
|
|
1596
|
+
return publicKey;
|
|
1597
|
+
}
|
|
1380
1598
|
async getBalance(forceRefresh = false) {
|
|
1381
1599
|
this.ensureConnected();
|
|
1382
1600
|
try {
|
|
@@ -1466,16 +1684,14 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1466
1684
|
if (!isValidAddress(txData.to)) {
|
|
1467
1685
|
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1468
1686
|
}
|
|
1469
|
-
|
|
1470
|
-
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1471
|
-
}
|
|
1687
|
+
const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
|
|
1472
1688
|
if (txData.message && txData.message.length > 1000) {
|
|
1473
1689
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1474
1690
|
}
|
|
1475
1691
|
try {
|
|
1476
1692
|
// log non-sensitive only
|
|
1477
1693
|
this.logger.log('Sending transaction:', { to: txData.to });
|
|
1478
|
-
const result = await this.communicator.sendRequest('send_transaction', txData);
|
|
1694
|
+
const result = await this.communicator.sendRequest('send_transaction', { ...txData, amount });
|
|
1479
1695
|
this.logger.log('Transaction result:', result);
|
|
1480
1696
|
// Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
|
|
1481
1697
|
if (result.accepted ?? result.success) {
|
|
@@ -1504,15 +1720,13 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1504
1720
|
if (!isValidAddress(txData.to)) {
|
|
1505
1721
|
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1506
1722
|
}
|
|
1507
|
-
|
|
1508
|
-
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1509
|
-
}
|
|
1723
|
+
const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
|
|
1510
1724
|
if (txData.message && txData.message.length > 1000) {
|
|
1511
1725
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1512
1726
|
}
|
|
1513
1727
|
try {
|
|
1514
1728
|
this.logger.log('Requesting transaction signature:', { to: txData.to });
|
|
1515
|
-
const result = await this.communicator.sendRequest('sign_transaction', txData);
|
|
1729
|
+
const result = await this.communicator.sendRequest('sign_transaction', { ...txData, amount });
|
|
1516
1730
|
return result;
|
|
1517
1731
|
}
|
|
1518
1732
|
catch (error) {
|
|
@@ -1556,9 +1770,15 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1556
1770
|
if (callData.method.length > 200) {
|
|
1557
1771
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
|
|
1558
1772
|
}
|
|
1773
|
+
if (callData.amount != null && callData.amountOct != null) {
|
|
1774
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Contract call amount: pass amount or amountOct, not both');
|
|
1775
|
+
}
|
|
1559
1776
|
if (callData.amount != null) {
|
|
1560
1777
|
this.assertExactOCTAmount(callData.amount, 'Contract call amount');
|
|
1561
1778
|
}
|
|
1779
|
+
if (callData.amountOct != null) {
|
|
1780
|
+
this.assertExactOCTAmount(callData.amountOct, 'Contract call amount');
|
|
1781
|
+
}
|
|
1562
1782
|
try {
|
|
1563
1783
|
if (JSON.stringify(callData.params).length > 65536) {
|
|
1564
1784
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
|
|
@@ -1576,7 +1796,11 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1576
1796
|
contract: callData.contract,
|
|
1577
1797
|
method: callData.method,
|
|
1578
1798
|
params: callData.params,
|
|
1579
|
-
amount: callData.
|
|
1799
|
+
amount: callData.amountOct != null
|
|
1800
|
+
? octToMicro(callData.amountOct)
|
|
1801
|
+
: callData.amount != null
|
|
1802
|
+
? String(callData.amount)
|
|
1803
|
+
: '0',
|
|
1580
1804
|
ou: callData.ou != null ? String(callData.ou) : '10000',
|
|
1581
1805
|
});
|
|
1582
1806
|
this.logger.log('Contract call result:', result);
|
|
@@ -1693,7 +1917,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1693
1917
|
}
|
|
1694
1918
|
}
|
|
1695
1919
|
/**
|
|
1696
|
-
* Encrypt public balance to private
|
|
1920
|
+
* Encrypt public balance to private.
|
|
1921
|
+
* @deprecated The 0xio extension does not serve this through the bridge (it answers
|
|
1922
|
+
* NOT_AVAILABLE); users encrypt from the extension's Privacy screen.
|
|
1697
1923
|
*/
|
|
1698
1924
|
async encryptBalance(amount) {
|
|
1699
1925
|
this.ensureConnected();
|
|
@@ -1714,7 +1940,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1714
1940
|
}
|
|
1715
1941
|
}
|
|
1716
1942
|
/**
|
|
1717
|
-
* Decrypt private balance to public
|
|
1943
|
+
* Decrypt private balance to public.
|
|
1944
|
+
* @deprecated The 0xio extension does not serve this through the bridge (it answers
|
|
1945
|
+
* NOT_AVAILABLE); users decrypt from the extension's Privacy screen.
|
|
1718
1946
|
*/
|
|
1719
1947
|
async decryptBalance(amount) {
|
|
1720
1948
|
this.ensureConnected();
|
|
@@ -1756,7 +1984,10 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1756
1984
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
|
|
1757
1985
|
}
|
|
1758
1986
|
try {
|
|
1759
|
-
const result = await this.communicator.sendRequest('send_private_transfer',
|
|
1987
|
+
const result = await this.communicator.sendRequest('send_private_transfer', {
|
|
1988
|
+
...transferData,
|
|
1989
|
+
...(transferData.amountRaw ? { amount_raw: transferData.amountRaw } : {}),
|
|
1990
|
+
});
|
|
1760
1991
|
// Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
|
|
1761
1992
|
if (result.accepted ?? result.success) {
|
|
1762
1993
|
setTimeout(() => {
|
|
@@ -1810,16 +2041,89 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1810
2041
|
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
|
|
1811
2042
|
}
|
|
1812
2043
|
}
|
|
2044
|
+
// Generic provider passthrough and RFP private primitives (since 2.8.0)
|
|
2045
|
+
// These route through the wallet bridge. The wallet keeps all private/FHE
|
|
2046
|
+
// secret material internal and returns only ciphertexts/proofs/tx hashes.
|
|
2047
|
+
/**
|
|
2048
|
+
* Send any wallet method + params through the bridge. Escape hatch for
|
|
2049
|
+
* primitives that don't have a typed helper yet (no SDK upgrade needed).
|
|
2050
|
+
* @since 2.8.0
|
|
2051
|
+
*/
|
|
2052
|
+
async request(method, params = {}) {
|
|
2053
|
+
return this.communicator.sendRequest(method, params);
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Read-only node RPC through the wallet. Only the wallet's allow-listed public methods work
|
|
2057
|
+
* (octra_balance, octra_transaction, contract_call and similar); writes are refused.
|
|
2058
|
+
* @since 2.8.0
|
|
2059
|
+
*/
|
|
2060
|
+
async rpcCall(method, params = []) {
|
|
2061
|
+
return this.communicator.sendRequest('rpc_call', { method, params });
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Feature-detect which private capabilities the connected wallet supports.
|
|
2065
|
+
* Lets a dapp render the correct UI (or fail closed) before any action.
|
|
2066
|
+
* @since 2.8.0
|
|
2067
|
+
*/
|
|
2068
|
+
async getPrivateCapabilities() {
|
|
2069
|
+
return this.communicator.sendRequest('get_private_capabilities');
|
|
2070
|
+
}
|
|
2071
|
+
/** Read-only contract view (no approval popup). @since 2.8.0 */
|
|
2072
|
+
async callContractView(params) {
|
|
2073
|
+
this.ensureConnected();
|
|
2074
|
+
return this.communicator.sendRequest('contract_call_view', params);
|
|
2075
|
+
}
|
|
2076
|
+
/** Encrypt a raw integer value to a contract-ready ciphertext (keys stay in wallet). @since 2.8.0 */
|
|
2077
|
+
async encryptValue(params) {
|
|
2078
|
+
this.ensureConnected();
|
|
2079
|
+
return this.communicator.sendRequest('encrypt_value', params);
|
|
2080
|
+
}
|
|
2081
|
+
/** Decrypt a ciphertext to a raw integer value. @since 2.8.0 */
|
|
2082
|
+
async decryptValue(params) {
|
|
2083
|
+
this.ensureConnected();
|
|
2084
|
+
return this.communicator.sendRequest('decrypt_value', params);
|
|
2085
|
+
}
|
|
2086
|
+
/** Zero proof for a ciphertext (proves it encrypts the given value, default 0). @since 2.8.0 */
|
|
2087
|
+
async makeZeroProof(params) {
|
|
2088
|
+
this.ensureConnected();
|
|
2089
|
+
return this.communicator.sendRequest('make_zero_proof', params);
|
|
2090
|
+
}
|
|
2091
|
+
/** Range proof for a ciphertext. @since 2.8.0 */
|
|
2092
|
+
async makeRangeProof(params) {
|
|
2093
|
+
this.ensureConnected();
|
|
2094
|
+
return this.communicator.sendRequest('make_range_proof', params);
|
|
2095
|
+
}
|
|
2096
|
+
/** Private OCT + private token balances (decrypted inside the wallet). @since 2.8.0 */
|
|
2097
|
+
async getPrivateBalance(params = {}) {
|
|
2098
|
+
this.ensureConnected();
|
|
2099
|
+
return this.communicator.sendRequest('get_private_balance', params);
|
|
2100
|
+
}
|
|
2101
|
+
/** Register a receive/view public key so an address can receive private transfers. @since 2.8.0 */
|
|
2102
|
+
async registerPrivateViewKey(params) {
|
|
2103
|
+
this.ensureConnected();
|
|
2104
|
+
return this.communicator.sendRequest('register_private_view_key', params);
|
|
2105
|
+
}
|
|
2106
|
+
/** Submit an ordered multi-step public contract flow under one approval. @since 2.8.0 */
|
|
2107
|
+
async sendContractTransactionSequence(params) {
|
|
2108
|
+
this.ensureConnected();
|
|
2109
|
+
return this.communicator.sendRequest('send_contract_transaction_sequence', params);
|
|
2110
|
+
}
|
|
1813
2111
|
/**
|
|
1814
|
-
* Sign an arbitrary message with the wallet's private key
|
|
1815
|
-
* The user will be prompted to approve the signature request in the extension
|
|
2112
|
+
* Sign an arbitrary message with the wallet's private key.
|
|
2113
|
+
* The user will be prompted to approve the signature request in the extension.
|
|
2114
|
+
*
|
|
2115
|
+
* The wallet does not sign the raw message: it signs the 0xio Signed Message framing
|
|
2116
|
+
* (`"Octra Signed Message:\n" + byteLength + "\n" + message`) so a signed message can never be a
|
|
2117
|
+
* transaction pre-image. Verify with `verifyMessage(message, signature, publicKey)`, or check an
|
|
2118
|
+
* Ed25519 signature against `getSignedMessageBytes(message)` with your own library.
|
|
2119
|
+
*
|
|
1816
2120
|
* @param message - The message to sign (non-empty string)
|
|
1817
2121
|
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1818
2122
|
* @throws ZeroXIOWalletError with code SIGNATURE_FAILED if signing fails
|
|
1819
2123
|
* @example
|
|
1820
2124
|
* ```typescript
|
|
1821
2125
|
* const signature = await wallet.signMessage('Hello, 0xio!');
|
|
1822
|
-
*
|
|
2126
|
+
* const ok = await verifyMessage('Hello, 0xio!', signature, await wallet.getPublicKey());
|
|
1823
2127
|
* ```
|
|
1824
2128
|
*/
|
|
1825
2129
|
async signMessage(message) {
|
|
@@ -1853,7 +2157,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1853
2157
|
* to the calling service and a one-time nonce, preventing cross-service replay attacks.
|
|
1854
2158
|
*
|
|
1855
2159
|
* @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
|
|
1856
|
-
* @param nonce - Unique one-time value
|
|
2160
|
+
* @param nonce - Unique one-time value. Use a server-generated UUID or challenge
|
|
1857
2161
|
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1858
2162
|
*/
|
|
1859
2163
|
async signAuthMessage(service, nonce) {
|
|
@@ -1865,8 +2169,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1865
2169
|
throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
|
|
1866
2170
|
}
|
|
1867
2171
|
const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
|
|
1868
|
-
|
|
1869
|
-
return this.signMessage(domainSeparated);
|
|
2172
|
+
return this.signMessage(buildAuthMessage(service, nonce, origin));
|
|
1870
2173
|
}
|
|
1871
2174
|
ensureInitialized() {
|
|
1872
2175
|
if (!this.isInitialized) {
|
|
@@ -1898,6 +2201,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1898
2201
|
this.communicator.on('transactionConfirmed', (event) => {
|
|
1899
2202
|
this.handleTransactionConfirmed(event.data);
|
|
1900
2203
|
});
|
|
2204
|
+
this.communicator.on('transactionFailed', (event) => {
|
|
2205
|
+
this.emit('transactionFailed', event.data ?? event);
|
|
2206
|
+
});
|
|
1901
2207
|
this.communicator.on('permissionsChanged', (event) => {
|
|
1902
2208
|
const permissions = event.data ?? event;
|
|
1903
2209
|
if (this.connectionInfo.isConnected) {
|
|
@@ -1936,7 +2242,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1936
2242
|
}
|
|
1937
2243
|
handleNetworkChanged(data) {
|
|
1938
2244
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1939
|
-
// validate networkInfo
|
|
2245
|
+
// validate networkInfo, drop invalid
|
|
1940
2246
|
const networkInfo = validateNetworkInfo(data.networkInfo);
|
|
1941
2247
|
if (!networkInfo) {
|
|
1942
2248
|
this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
|
|
@@ -1997,9 +2303,26 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1997
2303
|
}, 2000);
|
|
1998
2304
|
this.logger.log('Transaction confirmed:', data.txHash);
|
|
1999
2305
|
}
|
|
2306
|
+
/**
|
|
2307
|
+
* The wallet takes raw micro-OCT. `amount` passes through unchanged (what every existing
|
|
2308
|
+
* dapp sends today); `amountOct` is converted exactly. Never both.
|
|
2309
|
+
*/
|
|
2310
|
+
resolveRawAmount(amount, amountOct, label) {
|
|
2311
|
+
if (amount != null && amountOct != null) {
|
|
2312
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, `${label}: pass amount or amountOct, not both`);
|
|
2313
|
+
}
|
|
2314
|
+
if (amountOct != null) {
|
|
2315
|
+
this.assertExactOCTAmount(amountOct, label);
|
|
2316
|
+
return octToMicro(amountOct);
|
|
2317
|
+
}
|
|
2318
|
+
if (amount == null || !isValidAmount(amount)) {
|
|
2319
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, `Invalid ${label.toLowerCase()}`);
|
|
2320
|
+
}
|
|
2321
|
+
return amount;
|
|
2322
|
+
}
|
|
2000
2323
|
/**
|
|
2001
2324
|
* Reject numeric amounts that cannot be represented exactly in micro-OCT.
|
|
2002
|
-
* e.g. 0.1 + 0.2 = 0.30000000000000004
|
|
2325
|
+
* e.g. 0.1 + 0.2 = 0.30000000000000004: the extension would sign the wrong value.
|
|
2003
2326
|
* String amounts bypass this check (caller is responsible for correctness).
|
|
2004
2327
|
*/
|
|
2005
2328
|
assertExactOCTAmount(amount, label) {
|
|
@@ -2033,7 +2356,7 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
2033
2356
|
*
|
|
2034
2357
|
* Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
|
|
2035
2358
|
* window.octra.isOctra === true
|
|
2036
|
-
* window.octra.request({ method, params })
|
|
2359
|
+
* window.octra.request({ method, params }) returns Promise<unknown>
|
|
2037
2360
|
* window.octra.on(event, listener) / removeListener(event, listener)
|
|
2038
2361
|
*
|
|
2039
2362
|
* This adapter translates the SDK's internal method names into RFC-O-1 method
|
|
@@ -2042,7 +2365,7 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
2042
2365
|
* Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
|
|
2043
2366
|
* are passed through as-is; the wallet's request() handles or rejects them.
|
|
2044
2367
|
*/
|
|
2045
|
-
/** SDK method
|
|
2368
|
+
/** SDK method to RFC-O-1 method name */
|
|
2046
2369
|
const SDK_TO_RFC = {
|
|
2047
2370
|
get_network_info: 'octra_networkInfo',
|
|
2048
2371
|
switch_network: 'octra_switchNetwork',
|
|
@@ -2057,8 +2380,16 @@ const SDK_TO_RFC = {
|
|
|
2057
2380
|
decrypt_balance: 'octra_decryptBalance',
|
|
2058
2381
|
send_private_transfer: 'octra_sendPrivateTransfer',
|
|
2059
2382
|
claim_private_transfer: 'octra_claimStealth',
|
|
2383
|
+
get_private_capabilities: 'octra_getPrivateCapabilities',
|
|
2384
|
+
encrypt_value: 'octra_encryptValue',
|
|
2385
|
+
decrypt_value: 'octra_decryptValue',
|
|
2386
|
+
make_zero_proof: 'octra_makeZeroProof',
|
|
2387
|
+
make_range_proof: 'octra_makeRangeProof',
|
|
2388
|
+
get_private_balance: 'octra_getPrivateBalance',
|
|
2389
|
+
register_private_view_key: 'octra_registerPrivateViewKey',
|
|
2390
|
+
send_contract_transaction_sequence: 'octra_sendContractTransactionSequence',
|
|
2060
2391
|
};
|
|
2061
|
-
/** RFC-O-1 error code
|
|
2392
|
+
/** RFC-O-1 error code to SDK ErrorCode string */
|
|
2062
2393
|
const RFC_TO_SDK_ERROR = {
|
|
2063
2394
|
4001: 'USER_REJECTED',
|
|
2064
2395
|
4100: 'PERMISSION_DENIED',
|
|
@@ -2160,7 +2491,7 @@ function createOctraProviderAdapter() {
|
|
|
2160
2491
|
const provider = getProvider();
|
|
2161
2492
|
if (!provider)
|
|
2162
2493
|
return () => { _handler = null; };
|
|
2163
|
-
// RFC-O-1 event
|
|
2494
|
+
// RFC-O-1 event to SDK event name and data shape
|
|
2164
2495
|
const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
|
|
2165
2496
|
const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
|
|
2166
2497
|
const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
|
|
@@ -2202,15 +2533,15 @@ function createOctraProviderAdapter() {
|
|
|
2202
2533
|
const OctraProviderAdapter = createOctraProviderAdapter();
|
|
2203
2534
|
|
|
2204
2535
|
/**
|
|
2205
|
-
* 0xio SDK
|
|
2536
|
+
* 0xio SDK: Wallet Adapter Registry
|
|
2206
2537
|
*
|
|
2207
2538
|
* Add new wallet adapters here. Detection order determines which wallet takes
|
|
2208
2539
|
* priority when multiple wallets are installed at the same time.
|
|
2209
2540
|
*/
|
|
2210
2541
|
const REGISTERED_ADAPTERS = [
|
|
2211
|
-
ZeroXIOAdapter, // 0xio extension (postMessage protocol)
|
|
2542
|
+
ZeroXIOAdapter, // 0xio extension (postMessage protocol), highest priority
|
|
2212
2543
|
OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
|
|
2213
|
-
// Add new wallet adapters here
|
|
2544
|
+
// Add new wallet adapters here: detection runs in order, first match wins
|
|
2214
2545
|
];
|
|
2215
2546
|
/**
|
|
2216
2547
|
* Auto-detects the first available wallet in the current page.
|
|
@@ -2257,7 +2588,7 @@ function getAllAdapters() {
|
|
|
2257
2588
|
*/
|
|
2258
2589
|
// Main exports
|
|
2259
2590
|
// Version information
|
|
2260
|
-
const SDK_VERSION = '2.
|
|
2591
|
+
const SDK_VERSION = '2.8.0';
|
|
2261
2592
|
const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
|
|
2262
2593
|
const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
|
|
2263
2594
|
const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
|
|
@@ -2285,7 +2616,7 @@ async function createZeroXIOWallet(config) {
|
|
|
2285
2616
|
function checkSDKCompatibility() {
|
|
2286
2617
|
const issues = [];
|
|
2287
2618
|
const recommendations = [];
|
|
2288
|
-
// Hard blockers
|
|
2619
|
+
// Hard blockers: the SDK cannot function without these
|
|
2289
2620
|
if (typeof window === 'undefined') {
|
|
2290
2621
|
issues.push('Window object not available');
|
|
2291
2622
|
recommendations.push('SDK must be used in a browser environment');
|
|
@@ -2350,7 +2681,7 @@ if (typeof window !== 'undefined') {
|
|
|
2350
2681
|
debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
|
|
2351
2682
|
environment: isDevelopment ? 'development' : 'production'
|
|
2352
2683
|
}),
|
|
2353
|
-
// simulateExtensionEvent removed for security
|
|
2684
|
+
// simulateExtensionEvent removed for security: it could be exploited on staging builds
|
|
2354
2685
|
showWelcome: () => {
|
|
2355
2686
|
console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
|
|
2356
2687
|
console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
|
|
@@ -2368,16 +2699,21 @@ if (typeof window !== 'undefined') {
|
|
|
2368
2699
|
exports.DEFAULT_NETWORK_ID = DEFAULT_NETWORK_ID;
|
|
2369
2700
|
exports.EventEmitter = EventEmitter;
|
|
2370
2701
|
exports.ExtensionCommunicator = ExtensionCommunicator;
|
|
2702
|
+
exports.LEGACY_PERMISSION_MAP = LEGACY_PERMISSION_MAP;
|
|
2371
2703
|
exports.MIN_EXTENSION_VERSION = MIN_EXTENSION_VERSION;
|
|
2372
2704
|
exports.MIN_EXTENSION_VERSION_DEVNET = MIN_EXTENSION_VERSION_DEVNET;
|
|
2373
2705
|
exports.NETWORKS = NETWORKS;
|
|
2374
2706
|
exports.OctraProviderAdapter = OctraProviderAdapter;
|
|
2375
2707
|
exports.SDK_CONFIG = SDK_CONFIG;
|
|
2376
2708
|
exports.SDK_VERSION = SDK_VERSION;
|
|
2709
|
+
exports.SIGNED_MESSAGE_PREFIX = SIGNED_MESSAGE_PREFIX;
|
|
2710
|
+
exports.SIGNED_MESSAGE_VERSION = SIGNED_MESSAGE_VERSION;
|
|
2377
2711
|
exports.SUPPORTED_EXTENSION_VERSIONS = SUPPORTED_EXTENSION_VERSIONS;
|
|
2712
|
+
exports.WALLET_PERMISSIONS = WALLET_PERMISSIONS;
|
|
2378
2713
|
exports.ZeroXIOAdapter = ZeroXIOAdapter;
|
|
2379
2714
|
exports.ZeroXIOWallet = ZeroXIOWallet;
|
|
2380
2715
|
exports.ZeroXIOWalletError = ZeroXIOWalletError;
|
|
2716
|
+
exports.buildAuthMessage = buildAuthMessage;
|
|
2381
2717
|
exports.checkBrowserSupport = checkBrowserSupport;
|
|
2382
2718
|
exports.checkSDKCompatibility = checkSDKCompatibility;
|
|
2383
2719
|
exports.createDefaultBalance = createDefaultBalance;
|
|
@@ -2401,6 +2737,7 @@ exports.getAllAdapters = getAllAdapters;
|
|
|
2401
2737
|
exports.getAllNetworks = getAllNetworks;
|
|
2402
2738
|
exports.getDefaultNetwork = getDefaultNetwork;
|
|
2403
2739
|
exports.getNetworkConfig = getNetworkConfig;
|
|
2740
|
+
exports.getSignedMessageBytes = getSignedMessageBytes;
|
|
2404
2741
|
exports.isBrowser = isBrowser;
|
|
2405
2742
|
exports.isErrorType = isErrorType;
|
|
2406
2743
|
exports.isValidAddress = isValidAddress;
|
|
@@ -2408,6 +2745,10 @@ exports.isValidAmount = isValidAmount;
|
|
|
2408
2745
|
exports.isValidFeeLevel = isValidFeeLevel;
|
|
2409
2746
|
exports.isValidMessage = isValidMessage;
|
|
2410
2747
|
exports.isValidNetworkId = isValidNetworkId;
|
|
2748
|
+
exports.octToMicro = octToMicro;
|
|
2411
2749
|
exports.toMicroOCT = toMicroOCT;
|
|
2412
2750
|
exports.toMicroZeroXIO = toMicroOCT;
|
|
2751
|
+
exports.toWalletPermissions = toWalletPermissions;
|
|
2752
|
+
exports.verifyMessage = verifyMessage;
|
|
2753
|
+
exports.withLegacyAliases = withLegacyAliases;
|
|
2413
2754
|
//# sourceMappingURL=index.js.map
|