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