@0xio/sdk 2.7.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -39
- package/README.md +104 -280
- package/dist/index.d.ts +252 -44
- package/dist/index.esm.js +408 -78
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +417 -77
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +417 -77
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/index.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,8 +1540,8 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1339
1540
|
}
|
|
1340
1541
|
}
|
|
1341
1542
|
/**
|
|
1342
|
-
* Switch the extension's active network (e.g. 'mainnet'
|
|
1343
|
-
* Works silently
|
|
1543
|
+
* Switch the extension's active network (e.g. 'mainnet' to 'devnet').
|
|
1544
|
+
* Works silently: no popup, no user confirmation needed.
|
|
1344
1545
|
* The extension broadcasts 'networkChanged' event to all connected dApps.
|
|
1345
1546
|
*/
|
|
1346
1547
|
async switchNetwork(networkId) {
|
|
@@ -1375,6 +1576,22 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1375
1576
|
getAddress() {
|
|
1376
1577
|
return this.connectionInfo.address || null;
|
|
1377
1578
|
}
|
|
1579
|
+
/**
|
|
1580
|
+
* The connected account's Ed25519 public key (base64). Served from the session when the
|
|
1581
|
+
* wallet reported it at connect, otherwise asked from the wallet.
|
|
1582
|
+
*/
|
|
1583
|
+
async getPublicKey() {
|
|
1584
|
+
this.ensureConnected();
|
|
1585
|
+
if (this.connectionInfo.publicKey)
|
|
1586
|
+
return this.connectionInfo.publicKey;
|
|
1587
|
+
const result = await this.communicator.sendRequest('getPublicKey');
|
|
1588
|
+
const publicKey = result?.publicKey;
|
|
1589
|
+
if (typeof publicKey !== 'string' || !publicKey) {
|
|
1590
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Wallet did not return a public key');
|
|
1591
|
+
}
|
|
1592
|
+
this.connectionInfo.publicKey = publicKey;
|
|
1593
|
+
return publicKey;
|
|
1594
|
+
}
|
|
1378
1595
|
async getBalance(forceRefresh = false) {
|
|
1379
1596
|
this.ensureConnected();
|
|
1380
1597
|
try {
|
|
@@ -1464,16 +1681,14 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1464
1681
|
if (!isValidAddress(txData.to)) {
|
|
1465
1682
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1466
1683
|
}
|
|
1467
|
-
|
|
1468
|
-
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1469
|
-
}
|
|
1684
|
+
const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
|
|
1470
1685
|
if (txData.message && txData.message.length > 1000) {
|
|
1471
1686
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1472
1687
|
}
|
|
1473
1688
|
try {
|
|
1474
1689
|
// log non-sensitive only
|
|
1475
1690
|
this.logger.log('Sending transaction:', { to: txData.to });
|
|
1476
|
-
const result = await this.communicator.sendRequest('send_transaction', txData);
|
|
1691
|
+
const result = await this.communicator.sendRequest('send_transaction', { ...txData, amount });
|
|
1477
1692
|
this.logger.log('Transaction result:', result);
|
|
1478
1693
|
// Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
|
|
1479
1694
|
if (result.accepted ?? result.success) {
|
|
@@ -1502,15 +1717,13 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1502
1717
|
if (!isValidAddress(txData.to)) {
|
|
1503
1718
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1504
1719
|
}
|
|
1505
|
-
|
|
1506
|
-
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1507
|
-
}
|
|
1720
|
+
const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
|
|
1508
1721
|
if (txData.message && txData.message.length > 1000) {
|
|
1509
1722
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1510
1723
|
}
|
|
1511
1724
|
try {
|
|
1512
1725
|
this.logger.log('Requesting transaction signature:', { to: txData.to });
|
|
1513
|
-
const result = await this.communicator.sendRequest('sign_transaction', txData);
|
|
1726
|
+
const result = await this.communicator.sendRequest('sign_transaction', { ...txData, amount });
|
|
1514
1727
|
return result;
|
|
1515
1728
|
}
|
|
1516
1729
|
catch (error) {
|
|
@@ -1554,9 +1767,15 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1554
1767
|
if (callData.method.length > 200) {
|
|
1555
1768
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
|
|
1556
1769
|
}
|
|
1770
|
+
if (callData.amount != null && callData.amountOct != null) {
|
|
1771
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Contract call amount: pass amount or amountOct, not both');
|
|
1772
|
+
}
|
|
1557
1773
|
if (callData.amount != null) {
|
|
1558
1774
|
this.assertExactOCTAmount(callData.amount, 'Contract call amount');
|
|
1559
1775
|
}
|
|
1776
|
+
if (callData.amountOct != null) {
|
|
1777
|
+
this.assertExactOCTAmount(callData.amountOct, 'Contract call amount');
|
|
1778
|
+
}
|
|
1560
1779
|
try {
|
|
1561
1780
|
if (JSON.stringify(callData.params).length > 65536) {
|
|
1562
1781
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
|
|
@@ -1574,7 +1793,11 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1574
1793
|
contract: callData.contract,
|
|
1575
1794
|
method: callData.method,
|
|
1576
1795
|
params: callData.params,
|
|
1577
|
-
amount: callData.
|
|
1796
|
+
amount: callData.amountOct != null
|
|
1797
|
+
? octToMicro(callData.amountOct)
|
|
1798
|
+
: callData.amount != null
|
|
1799
|
+
? String(callData.amount)
|
|
1800
|
+
: '0',
|
|
1578
1801
|
ou: callData.ou != null ? String(callData.ou) : '10000',
|
|
1579
1802
|
});
|
|
1580
1803
|
this.logger.log('Contract call result:', result);
|
|
@@ -1691,7 +1914,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1691
1914
|
}
|
|
1692
1915
|
}
|
|
1693
1916
|
/**
|
|
1694
|
-
* Encrypt public balance to private
|
|
1917
|
+
* Encrypt public balance to private.
|
|
1918
|
+
* @deprecated The 0xio extension does not serve this through the bridge (it answers
|
|
1919
|
+
* NOT_AVAILABLE); users encrypt from the extension's Privacy screen.
|
|
1695
1920
|
*/
|
|
1696
1921
|
async encryptBalance(amount) {
|
|
1697
1922
|
this.ensureConnected();
|
|
@@ -1712,7 +1937,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1712
1937
|
}
|
|
1713
1938
|
}
|
|
1714
1939
|
/**
|
|
1715
|
-
* Decrypt private balance to public
|
|
1940
|
+
* Decrypt private balance to public.
|
|
1941
|
+
* @deprecated The 0xio extension does not serve this through the bridge (it answers
|
|
1942
|
+
* NOT_AVAILABLE); users decrypt from the extension's Privacy screen.
|
|
1716
1943
|
*/
|
|
1717
1944
|
async decryptBalance(amount) {
|
|
1718
1945
|
this.ensureConnected();
|
|
@@ -1754,7 +1981,10 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1754
1981
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
|
|
1755
1982
|
}
|
|
1756
1983
|
try {
|
|
1757
|
-
const result = await this.communicator.sendRequest('send_private_transfer',
|
|
1984
|
+
const result = await this.communicator.sendRequest('send_private_transfer', {
|
|
1985
|
+
...transferData,
|
|
1986
|
+
...(transferData.amountRaw ? { amount_raw: transferData.amountRaw } : {}),
|
|
1987
|
+
});
|
|
1758
1988
|
// Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
|
|
1759
1989
|
if (result.accepted ?? result.success) {
|
|
1760
1990
|
setTimeout(() => {
|
|
@@ -1808,16 +2038,89 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1808
2038
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
|
|
1809
2039
|
}
|
|
1810
2040
|
}
|
|
2041
|
+
// Generic provider passthrough and RFP private primitives (since 2.8.0)
|
|
2042
|
+
// These route through the wallet bridge. The wallet keeps all private/FHE
|
|
2043
|
+
// secret material internal and returns only ciphertexts/proofs/tx hashes.
|
|
2044
|
+
/**
|
|
2045
|
+
* Send any wallet method + params through the bridge. Escape hatch for
|
|
2046
|
+
* primitives that don't have a typed helper yet (no SDK upgrade needed).
|
|
2047
|
+
* @since 2.8.0
|
|
2048
|
+
*/
|
|
2049
|
+
async request(method, params = {}) {
|
|
2050
|
+
return this.communicator.sendRequest(method, params);
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Read-only node RPC through the wallet. Only the wallet's allow-listed public methods work
|
|
2054
|
+
* (octra_balance, octra_transaction, contract_call and similar); writes are refused.
|
|
2055
|
+
* @since 2.8.0
|
|
2056
|
+
*/
|
|
2057
|
+
async rpcCall(method, params = []) {
|
|
2058
|
+
return this.communicator.sendRequest('rpc_call', { method, params });
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Feature-detect which private capabilities the connected wallet supports.
|
|
2062
|
+
* Lets a dapp render the correct UI (or fail closed) before any action.
|
|
2063
|
+
* @since 2.8.0
|
|
2064
|
+
*/
|
|
2065
|
+
async getPrivateCapabilities() {
|
|
2066
|
+
return this.communicator.sendRequest('get_private_capabilities');
|
|
2067
|
+
}
|
|
2068
|
+
/** Read-only contract view (no approval popup). @since 2.8.0 */
|
|
2069
|
+
async callContractView(params) {
|
|
2070
|
+
this.ensureConnected();
|
|
2071
|
+
return this.communicator.sendRequest('contract_call_view', params);
|
|
2072
|
+
}
|
|
2073
|
+
/** Encrypt a raw integer value to a contract-ready ciphertext (keys stay in wallet). @since 2.8.0 */
|
|
2074
|
+
async encryptValue(params) {
|
|
2075
|
+
this.ensureConnected();
|
|
2076
|
+
return this.communicator.sendRequest('encrypt_value', params);
|
|
2077
|
+
}
|
|
2078
|
+
/** Decrypt a ciphertext to a raw integer value. @since 2.8.0 */
|
|
2079
|
+
async decryptValue(params) {
|
|
2080
|
+
this.ensureConnected();
|
|
2081
|
+
return this.communicator.sendRequest('decrypt_value', params);
|
|
2082
|
+
}
|
|
2083
|
+
/** Zero proof for a ciphertext (proves it encrypts the given value, default 0). @since 2.8.0 */
|
|
2084
|
+
async makeZeroProof(params) {
|
|
2085
|
+
this.ensureConnected();
|
|
2086
|
+
return this.communicator.sendRequest('make_zero_proof', params);
|
|
2087
|
+
}
|
|
2088
|
+
/** Range proof for a ciphertext. @since 2.8.0 */
|
|
2089
|
+
async makeRangeProof(params) {
|
|
2090
|
+
this.ensureConnected();
|
|
2091
|
+
return this.communicator.sendRequest('make_range_proof', params);
|
|
2092
|
+
}
|
|
2093
|
+
/** Private OCT + private token balances (decrypted inside the wallet). @since 2.8.0 */
|
|
2094
|
+
async getPrivateBalance(params = {}) {
|
|
2095
|
+
this.ensureConnected();
|
|
2096
|
+
return this.communicator.sendRequest('get_private_balance', params);
|
|
2097
|
+
}
|
|
2098
|
+
/** Register a receive/view public key so an address can receive private transfers. @since 2.8.0 */
|
|
2099
|
+
async registerPrivateViewKey(params) {
|
|
2100
|
+
this.ensureConnected();
|
|
2101
|
+
return this.communicator.sendRequest('register_private_view_key', params);
|
|
2102
|
+
}
|
|
2103
|
+
/** Submit an ordered multi-step public contract flow under one approval. @since 2.8.0 */
|
|
2104
|
+
async sendContractTransactionSequence(params) {
|
|
2105
|
+
this.ensureConnected();
|
|
2106
|
+
return this.communicator.sendRequest('send_contract_transaction_sequence', params);
|
|
2107
|
+
}
|
|
1811
2108
|
/**
|
|
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
|
|
2109
|
+
* Sign an arbitrary message with the wallet's private key.
|
|
2110
|
+
* The user will be prompted to approve the signature request in the extension.
|
|
2111
|
+
*
|
|
2112
|
+
* The wallet does not sign the raw message: it signs the 0xio Signed Message framing
|
|
2113
|
+
* (`"Octra Signed Message:\n" + byteLength + "\n" + message`) so a signed message can never be a
|
|
2114
|
+
* transaction pre-image. Verify with `verifyMessage(message, signature, publicKey)`, or check an
|
|
2115
|
+
* Ed25519 signature against `getSignedMessageBytes(message)` with your own library.
|
|
2116
|
+
*
|
|
1814
2117
|
* @param message - The message to sign (non-empty string)
|
|
1815
2118
|
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1816
2119
|
* @throws ZeroXIOWalletError with code SIGNATURE_FAILED if signing fails
|
|
1817
2120
|
* @example
|
|
1818
2121
|
* ```typescript
|
|
1819
2122
|
* const signature = await wallet.signMessage('Hello, 0xio!');
|
|
1820
|
-
*
|
|
2123
|
+
* const ok = await verifyMessage('Hello, 0xio!', signature, await wallet.getPublicKey());
|
|
1821
2124
|
* ```
|
|
1822
2125
|
*/
|
|
1823
2126
|
async signMessage(message) {
|
|
@@ -1851,7 +2154,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1851
2154
|
* to the calling service and a one-time nonce, preventing cross-service replay attacks.
|
|
1852
2155
|
*
|
|
1853
2156
|
* @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
|
|
1854
|
-
* @param nonce - Unique one-time value
|
|
2157
|
+
* @param nonce - Unique one-time value. Use a server-generated UUID or challenge
|
|
1855
2158
|
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1856
2159
|
*/
|
|
1857
2160
|
async signAuthMessage(service, nonce) {
|
|
@@ -1863,8 +2166,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1863
2166
|
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
|
|
1864
2167
|
}
|
|
1865
2168
|
const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
|
|
1866
|
-
|
|
1867
|
-
return this.signMessage(domainSeparated);
|
|
2169
|
+
return this.signMessage(buildAuthMessage(service, nonce, origin));
|
|
1868
2170
|
}
|
|
1869
2171
|
ensureInitialized() {
|
|
1870
2172
|
if (!this.isInitialized) {
|
|
@@ -1896,6 +2198,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1896
2198
|
this.communicator.on('transactionConfirmed', (event) => {
|
|
1897
2199
|
this.handleTransactionConfirmed(event.data);
|
|
1898
2200
|
});
|
|
2201
|
+
this.communicator.on('transactionFailed', (event) => {
|
|
2202
|
+
this.emit('transactionFailed', event.data ?? event);
|
|
2203
|
+
});
|
|
1899
2204
|
this.communicator.on('permissionsChanged', (event) => {
|
|
1900
2205
|
const permissions = event.data ?? event;
|
|
1901
2206
|
if (this.connectionInfo.isConnected) {
|
|
@@ -1934,7 +2239,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1934
2239
|
}
|
|
1935
2240
|
handleNetworkChanged(data) {
|
|
1936
2241
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1937
|
-
// validate networkInfo
|
|
2242
|
+
// validate networkInfo, drop invalid
|
|
1938
2243
|
const networkInfo = validateNetworkInfo(data.networkInfo);
|
|
1939
2244
|
if (!networkInfo) {
|
|
1940
2245
|
this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
|
|
@@ -1995,9 +2300,26 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1995
2300
|
}, 2000);
|
|
1996
2301
|
this.logger.log('Transaction confirmed:', data.txHash);
|
|
1997
2302
|
}
|
|
2303
|
+
/**
|
|
2304
|
+
* The wallet takes raw micro-OCT. `amount` passes through unchanged (what every existing
|
|
2305
|
+
* dapp sends today); `amountOct` is converted exactly. Never both.
|
|
2306
|
+
*/
|
|
2307
|
+
resolveRawAmount(amount, amountOct, label) {
|
|
2308
|
+
if (amount != null && amountOct != null) {
|
|
2309
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, `${label}: pass amount or amountOct, not both`);
|
|
2310
|
+
}
|
|
2311
|
+
if (amountOct != null) {
|
|
2312
|
+
this.assertExactOCTAmount(amountOct, label);
|
|
2313
|
+
return octToMicro(amountOct);
|
|
2314
|
+
}
|
|
2315
|
+
if (amount == null || !isValidAmount(amount)) {
|
|
2316
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, `Invalid ${label.toLowerCase()}`);
|
|
2317
|
+
}
|
|
2318
|
+
return amount;
|
|
2319
|
+
}
|
|
1998
2320
|
/**
|
|
1999
2321
|
* Reject numeric amounts that cannot be represented exactly in micro-OCT.
|
|
2000
|
-
* e.g. 0.1 + 0.2 = 0.30000000000000004
|
|
2322
|
+
* e.g. 0.1 + 0.2 = 0.30000000000000004: the extension would sign the wrong value.
|
|
2001
2323
|
* String amounts bypass this check (caller is responsible for correctness).
|
|
2002
2324
|
*/
|
|
2003
2325
|
assertExactOCTAmount(amount, label) {
|
|
@@ -2031,7 +2353,7 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
2031
2353
|
*
|
|
2032
2354
|
* Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
|
|
2033
2355
|
* window.octra.isOctra === true
|
|
2034
|
-
* window.octra.request({ method, params })
|
|
2356
|
+
* window.octra.request({ method, params }) returns Promise<unknown>
|
|
2035
2357
|
* window.octra.on(event, listener) / removeListener(event, listener)
|
|
2036
2358
|
*
|
|
2037
2359
|
* This adapter translates the SDK's internal method names into RFC-O-1 method
|
|
@@ -2040,7 +2362,7 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
2040
2362
|
* Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
|
|
2041
2363
|
* are passed through as-is; the wallet's request() handles or rejects them.
|
|
2042
2364
|
*/
|
|
2043
|
-
/** SDK method
|
|
2365
|
+
/** SDK method to RFC-O-1 method name */
|
|
2044
2366
|
const SDK_TO_RFC = {
|
|
2045
2367
|
get_network_info: 'octra_networkInfo',
|
|
2046
2368
|
switch_network: 'octra_switchNetwork',
|
|
@@ -2055,8 +2377,16 @@ const SDK_TO_RFC = {
|
|
|
2055
2377
|
decrypt_balance: 'octra_decryptBalance',
|
|
2056
2378
|
send_private_transfer: 'octra_sendPrivateTransfer',
|
|
2057
2379
|
claim_private_transfer: 'octra_claimStealth',
|
|
2380
|
+
get_private_capabilities: 'octra_getPrivateCapabilities',
|
|
2381
|
+
encrypt_value: 'octra_encryptValue',
|
|
2382
|
+
decrypt_value: 'octra_decryptValue',
|
|
2383
|
+
make_zero_proof: 'octra_makeZeroProof',
|
|
2384
|
+
make_range_proof: 'octra_makeRangeProof',
|
|
2385
|
+
get_private_balance: 'octra_getPrivateBalance',
|
|
2386
|
+
register_private_view_key: 'octra_registerPrivateViewKey',
|
|
2387
|
+
send_contract_transaction_sequence: 'octra_sendContractTransactionSequence',
|
|
2058
2388
|
};
|
|
2059
|
-
/** RFC-O-1 error code
|
|
2389
|
+
/** RFC-O-1 error code to SDK ErrorCode string */
|
|
2060
2390
|
const RFC_TO_SDK_ERROR = {
|
|
2061
2391
|
4001: 'USER_REJECTED',
|
|
2062
2392
|
4100: 'PERMISSION_DENIED',
|
|
@@ -2158,7 +2488,7 @@ function createOctraProviderAdapter() {
|
|
|
2158
2488
|
const provider = getProvider();
|
|
2159
2489
|
if (!provider)
|
|
2160
2490
|
return () => { _handler = null; };
|
|
2161
|
-
// RFC-O-1 event
|
|
2491
|
+
// RFC-O-1 event to SDK event name and data shape
|
|
2162
2492
|
const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
|
|
2163
2493
|
const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
|
|
2164
2494
|
const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
|
|
@@ -2200,15 +2530,15 @@ function createOctraProviderAdapter() {
|
|
|
2200
2530
|
const OctraProviderAdapter = createOctraProviderAdapter();
|
|
2201
2531
|
|
|
2202
2532
|
/**
|
|
2203
|
-
* 0xio SDK
|
|
2533
|
+
* 0xio SDK: Wallet Adapter Registry
|
|
2204
2534
|
*
|
|
2205
2535
|
* Add new wallet adapters here. Detection order determines which wallet takes
|
|
2206
2536
|
* priority when multiple wallets are installed at the same time.
|
|
2207
2537
|
*/
|
|
2208
2538
|
const REGISTERED_ADAPTERS = [
|
|
2209
|
-
ZeroXIOAdapter, // 0xio extension (postMessage protocol)
|
|
2539
|
+
ZeroXIOAdapter, // 0xio extension (postMessage protocol), highest priority
|
|
2210
2540
|
OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
|
|
2211
|
-
// Add new wallet adapters here
|
|
2541
|
+
// Add new wallet adapters here: detection runs in order, first match wins
|
|
2212
2542
|
];
|
|
2213
2543
|
/**
|
|
2214
2544
|
* Auto-detects the first available wallet in the current page.
|
|
@@ -2255,7 +2585,7 @@ function getAllAdapters() {
|
|
|
2255
2585
|
*/
|
|
2256
2586
|
// Main exports
|
|
2257
2587
|
// Version information
|
|
2258
|
-
const SDK_VERSION = '2.
|
|
2588
|
+
const SDK_VERSION = '2.8.0';
|
|
2259
2589
|
const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
|
|
2260
2590
|
const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
|
|
2261
2591
|
const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
|
|
@@ -2283,7 +2613,7 @@ async function createZeroXIOWallet(config) {
|
|
|
2283
2613
|
function checkSDKCompatibility() {
|
|
2284
2614
|
const issues = [];
|
|
2285
2615
|
const recommendations = [];
|
|
2286
|
-
// Hard blockers
|
|
2616
|
+
// Hard blockers: the SDK cannot function without these
|
|
2287
2617
|
if (typeof window === 'undefined') {
|
|
2288
2618
|
issues.push('Window object not available');
|
|
2289
2619
|
recommendations.push('SDK must be used in a browser environment');
|
|
@@ -2348,7 +2678,7 @@ if (typeof window !== 'undefined') {
|
|
|
2348
2678
|
debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
|
|
2349
2679
|
environment: isDevelopment ? 'development' : 'production'
|
|
2350
2680
|
}),
|
|
2351
|
-
// simulateExtensionEvent removed for security
|
|
2681
|
+
// simulateExtensionEvent removed for security: it could be exploited on staging builds
|
|
2352
2682
|
showWelcome: () => {
|
|
2353
2683
|
console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
|
|
2354
2684
|
console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
|
|
@@ -2363,5 +2693,5 @@ if (typeof window !== 'undefined') {
|
|
|
2363
2693
|
}
|
|
2364
2694
|
}
|
|
2365
2695
|
|
|
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 };
|
|
2696
|
+
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
2697
|
//# sourceMappingURL=index.esm.js.map
|