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