@bounded-sh/core 0.0.42 → 0.0.44
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/dist/client/config.d.ts +20 -1
- package/dist/client/operations.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +726 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +720 -41
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +29 -5
- package/dist/utils/auth-api.d.ts +10 -1
- package/dist/utils/evm/evm-transaction-validation.d.ts +46 -0
- package/dist/utils/evm/evm-utils.d.ts +40 -0
- package/dist/utils/server-session-manager.d.ts +7 -0
- package/dist/utils/utils.d.ts +3 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -51,6 +51,10 @@ let clientConfig = {
|
|
|
51
51
|
isServer: false,
|
|
52
52
|
};
|
|
53
53
|
let isInitialized = false;
|
|
54
|
+
// Callers awaiting getConfig() before init() completes. Resolved synchronously
|
|
55
|
+
// by init() — the previous 50ms setInterval poll added up to 50ms of latency
|
|
56
|
+
// to the first SDK operation after init for no reason.
|
|
57
|
+
let initWaiters = [];
|
|
54
58
|
const BOUNDED_NETWORKS = {
|
|
55
59
|
'bounded-staging': {
|
|
56
60
|
wsApiUrl: 'wss://realtime-staging.bounded.sh',
|
|
@@ -119,23 +123,17 @@ function init(newConfig) {
|
|
|
119
123
|
const preset = newConfig.network ? BOUNDED_NETWORKS[newConfig.network] : {};
|
|
120
124
|
clientConfig = Object.assign(Object.assign(Object.assign({}, clientConfig), preset), newConfig);
|
|
121
125
|
isInitialized = true;
|
|
126
|
+
const waiters = initWaiters;
|
|
127
|
+
initWaiters = [];
|
|
128
|
+
for (const wake of waiters)
|
|
129
|
+
wake();
|
|
122
130
|
resolve();
|
|
123
131
|
});
|
|
124
132
|
}
|
|
125
133
|
function waitForInitialization() {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
const interval = setInterval(() => {
|
|
132
|
-
if (isInitialized) {
|
|
133
|
-
clearInterval(interval);
|
|
134
|
-
resolve();
|
|
135
|
-
}
|
|
136
|
-
}, 50);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
134
|
+
if (isInitialized)
|
|
135
|
+
return Promise.resolve();
|
|
136
|
+
return new Promise((resolve) => { initWaiters.push(resolve); });
|
|
139
137
|
}
|
|
140
138
|
async function getConfig() {
|
|
141
139
|
await waitForInitialization();
|
|
@@ -2821,8 +2819,24 @@ async function genAuthNonce() {
|
|
|
2821
2819
|
// stamps an is_anonymous claim. Without this a guest would be indistinguishable
|
|
2822
2820
|
// from 'phantom' and couldn't be gated or flagged.
|
|
2823
2821
|
const SIGNATURE_AUTH_METHODS = new Set(['phantom', 'mobile-wallet-adapter', 'guest']);
|
|
2824
|
-
|
|
2825
|
-
|
|
2822
|
+
// Maps a client authMethod to the /session issuer's signature discriminator when
|
|
2823
|
+
// they differ. EVM WALLET LOGIN (config.authMethod 'evm-wallet') exchanges a SIWE
|
|
2824
|
+
// (EIP-4361 personal_sign, secp256k1) signature, which the bounded-auth issuer
|
|
2825
|
+
// routes on authMethod:"siwe" (index.ts /session → parseSiweMessage/recoverSiweSigner)
|
|
2826
|
+
// and mints under custom:evmWalletAddress. The EVM provider also passes 'siwe'
|
|
2827
|
+
// explicitly (authMethodOverride), so this map is the belt-and-suspenders path.
|
|
2828
|
+
const SESSION_METHOD_ALIASES = { 'evm-wallet': 'siwe' };
|
|
2829
|
+
/**
|
|
2830
|
+
* Exchange a wallet signature for a Bounded session at the /session issuer.
|
|
2831
|
+
*
|
|
2832
|
+
* @param authMethodOverride the exact /session discriminator to send (e.g. 'siwe'
|
|
2833
|
+
* for the SIWE/EVM path). When omitted the method is derived from config: an
|
|
2834
|
+
* aliased method (evm-wallet → siwe) maps through SESSION_METHOD_ALIASES, a
|
|
2835
|
+
* Solana wallet/guest method passes through verbatim, and anything else
|
|
2836
|
+
* defaults to the canonical 'phantom' the issuer accepts for a signature.
|
|
2837
|
+
*/
|
|
2838
|
+
async function createSessionWithSignature(address, message, signature, authMethodOverride) {
|
|
2839
|
+
var _a, _b, _c;
|
|
2826
2840
|
const client = await getAxiosAuthClient();
|
|
2827
2841
|
const config = await getConfig();
|
|
2828
2842
|
const appId = config.appId;
|
|
@@ -2831,7 +2845,7 @@ async function createSessionWithSignature(address, message, signature) {
|
|
|
2831
2845
|
// canonical Solana wallet method 'phantom' that every /session issuer accepts
|
|
2832
2846
|
// for a signature exchange. This is what makes the server SDK + guest device
|
|
2833
2847
|
// keypair work under the email-default config.
|
|
2834
|
-
const authMethod = SIGNATURE_AUTH_METHODS.has(config.authMethod) ? config.authMethod : 'phantom';
|
|
2848
|
+
const authMethod = (_a = authMethodOverride !== null && authMethodOverride !== void 0 ? authMethodOverride : SESSION_METHOD_ALIASES[config.authMethod]) !== null && _a !== void 0 ? _a : (SIGNATURE_AUTH_METHODS.has(config.authMethod) ? config.authMethod : 'phantom');
|
|
2835
2849
|
try {
|
|
2836
2850
|
const response = await client.post('/session', {
|
|
2837
2851
|
appId,
|
|
@@ -2846,11 +2860,11 @@ async function createSessionWithSignature(address, message, signature) {
|
|
|
2846
2860
|
// Surface the issuer's actionable message (e.g. "Anonymous auth is not enabled for
|
|
2847
2861
|
// this app. Add \"auth\": { \"anonymous\": true } to policy.json and redeploy.")
|
|
2848
2862
|
// instead of axios's generic "Request failed with status code 403".
|
|
2849
|
-
const d = (
|
|
2863
|
+
const d = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.data;
|
|
2850
2864
|
if (d && (d.message || d.error)) {
|
|
2851
2865
|
const e = new Error(String(d.message || d.error));
|
|
2852
2866
|
e.code = d.error;
|
|
2853
|
-
e.status = (
|
|
2867
|
+
e.status = (_c = err === null || err === void 0 ? void 0 : err.response) === null || _c === void 0 ? void 0 : _c.status;
|
|
2854
2868
|
e.hint = d.hint;
|
|
2855
2869
|
throw e;
|
|
2856
2870
|
}
|
|
@@ -3625,12 +3639,13 @@ const NO_AMBIENT_SERVER_SESSION = "Server sessions are not process-global. Use c
|
|
|
3625
3639
|
/* SESSION MANAGER */
|
|
3626
3640
|
/* ------------------------------------------------------------------ */
|
|
3627
3641
|
class ServerSessionManager {
|
|
3628
|
-
constructor(keypair) {
|
|
3642
|
+
constructor(keypair, sessionFactory) {
|
|
3629
3643
|
/* Private cache (lives for the life of the instance) */
|
|
3630
3644
|
this.session = null;
|
|
3631
3645
|
/* Coalesce concurrent getSession() calls into a single in-flight request */
|
|
3632
3646
|
this.pendingSession = null;
|
|
3633
3647
|
this.keypair = keypair !== null && keypair !== void 0 ? keypair : null;
|
|
3648
|
+
this.sessionFactory = sessionFactory !== null && sessionFactory !== void 0 ? sessionFactory : null;
|
|
3634
3649
|
}
|
|
3635
3650
|
/**
|
|
3636
3651
|
* Create a session manager for a specific keypair.
|
|
@@ -3639,10 +3654,30 @@ class ServerSessionManager {
|
|
|
3639
3654
|
static forKeypair(keypair) {
|
|
3640
3655
|
return new ServerSessionManager(keypair);
|
|
3641
3656
|
}
|
|
3657
|
+
/**
|
|
3658
|
+
* Create an isolated server session manager for a non-Solana signer (currently
|
|
3659
|
+
* EVM/SIWE). The factory must perform the signature exchange and return the
|
|
3660
|
+
* canonical signer address with its tokens; no signer state becomes global.
|
|
3661
|
+
*/
|
|
3662
|
+
static forSessionFactory(factory) {
|
|
3663
|
+
if (typeof factory !== 'function')
|
|
3664
|
+
throw new Error('A server session factory is required');
|
|
3665
|
+
return new ServerSessionManager(undefined, factory);
|
|
3666
|
+
}
|
|
3642
3667
|
/* ---------------------------------------------- *
|
|
3643
3668
|
* Session creation (instance method)
|
|
3644
3669
|
* ---------------------------------------------- */
|
|
3645
3670
|
async createSession() {
|
|
3671
|
+
if (this.sessionFactory) {
|
|
3672
|
+
const session = await this.sessionFactory();
|
|
3673
|
+
if (!session || typeof session.address !== 'string' || !session.address ||
|
|
3674
|
+
typeof session.idToken !== 'string' || !session.idToken ||
|
|
3675
|
+
typeof session.accessToken !== 'string' || !session.accessToken ||
|
|
3676
|
+
typeof session.refreshToken !== 'string' || !session.refreshToken) {
|
|
3677
|
+
throw new Error('Server signer session factory returned an invalid session');
|
|
3678
|
+
}
|
|
3679
|
+
return session;
|
|
3680
|
+
}
|
|
3646
3681
|
if (!this.keypair) {
|
|
3647
3682
|
throw new Error(NO_AMBIENT_SERVER_SESSION);
|
|
3648
3683
|
}
|
|
@@ -3746,6 +3781,133 @@ function decodeBase64Url(input) {
|
|
|
3746
3781
|
return safeAtob(b64);
|
|
3747
3782
|
}
|
|
3748
3783
|
|
|
3784
|
+
// evm-utils.ts — the EVM twin of sol/sol-utils.ts's genSolanaMessage. It builds
|
|
3785
|
+
// the canonical EIP-4361 (Sign-In with Ethereum, SIWE) prompt the bounded-auth
|
|
3786
|
+
// issuer verifies, and normalizes EVM addresses to the single canonical form the
|
|
3787
|
+
// identity layer matches on.
|
|
3788
|
+
//
|
|
3789
|
+
// The SIWE message MUST match bounded-auth's parser byte-for-byte
|
|
3790
|
+
// (packages/cdk/cloudflare/bounded-auth/src/verify-siwe.ts → parseSiweMessage):
|
|
3791
|
+
// <domain> wants you to sign in with your Ethereum account:
|
|
3792
|
+
// <address>
|
|
3793
|
+
// <blank>
|
|
3794
|
+
// <statement>
|
|
3795
|
+
// <blank>
|
|
3796
|
+
// URI: <uri>
|
|
3797
|
+
// Version: 1
|
|
3798
|
+
// Chain ID: <chainId>
|
|
3799
|
+
// Nonce: <nonce>
|
|
3800
|
+
// Issued At: <ISO8601>
|
|
3801
|
+
// Expiration Time: <ISO8601>
|
|
3802
|
+
// Resources:
|
|
3803
|
+
// - bounded:app:<appId>
|
|
3804
|
+
//
|
|
3805
|
+
// Bindings the verifier enforces (so we produce them exactly): the address is
|
|
3806
|
+
// lowercased (identity match is case-sensitive, so lowercase-at-boundary is
|
|
3807
|
+
// mandatory), the single `bounded:app:<appId>` resource binds the signature to
|
|
3808
|
+
// THIS app (a signature for one app must not mint a session for another — the
|
|
3809
|
+
// SIWE analog of genSolanaMessage's `App ID:` line), and Expiration Time is
|
|
3810
|
+
// REQUIRED and must fall within the issuer's 10-minute max validity window.
|
|
3811
|
+
/** Default statement — kept identical to genSolanaMessage's for a consistent prompt. */
|
|
3812
|
+
const DEFAULT_STATEMENT = 'Sign this message to authenticate with our application.';
|
|
3813
|
+
/** Match bounded-auth's MAX_AUTH_WINDOW_MS (10 min); we default to a 5-min window. */
|
|
3814
|
+
const DEFAULT_VALIDITY_MS = 5 * 60 * 1000;
|
|
3815
|
+
/**
|
|
3816
|
+
* Build the canonical EIP-4361 SIWE message the bounded-auth issuer verifies.
|
|
3817
|
+
*
|
|
3818
|
+
* @param address the connected EVM account (lowercased into the prompt).
|
|
3819
|
+
* @param nonce single-use nonce from genAuthNonce().
|
|
3820
|
+
* @param appId the target app id (bound as the sole `bounded:app:<appId>` resource).
|
|
3821
|
+
* @param domain RFC-3986 authority of the requesting site (e.g. window.location.host).
|
|
3822
|
+
* @param uri the requesting URI; its origin is bound to the domain and actual browser Origin.
|
|
3823
|
+
*/
|
|
3824
|
+
function genSiweMessage(address, nonce, appId, domain, uri, options = {}) {
|
|
3825
|
+
var _a, _b, _c, _d, _e;
|
|
3826
|
+
const addr = normalizeEvmAddress(address);
|
|
3827
|
+
if (!addr)
|
|
3828
|
+
throw new Error('SIWE address must be a 20-byte 0x-prefixed EVM address');
|
|
3829
|
+
const statement = (_a = options.statement) !== null && _a !== void 0 ? _a : DEFAULT_STATEMENT;
|
|
3830
|
+
const version = (_b = options.version) !== null && _b !== void 0 ? _b : '1';
|
|
3831
|
+
const chainId = (_c = options.chainId) !== null && _c !== void 0 ? _c : 1;
|
|
3832
|
+
if (statement !== DEFAULT_STATEMENT)
|
|
3833
|
+
throw new Error('SIWE statement must use the canonical Bounded authentication text');
|
|
3834
|
+
if (version !== '1')
|
|
3835
|
+
throw new Error('SIWE version must be 1');
|
|
3836
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0)
|
|
3837
|
+
throw new Error('SIWE chainId must be a positive safe integer');
|
|
3838
|
+
if (!/^[A-Za-z0-9]{8,}$/.test(nonce))
|
|
3839
|
+
throw new Error('SIWE nonce must be at least 8 alphanumeric characters');
|
|
3840
|
+
if (!appId || /[\r\n]/.test(appId))
|
|
3841
|
+
throw new Error('SIWE appId is required and must be single-line');
|
|
3842
|
+
if (!domain || domain !== domain.trim() || /[\s/@?#\\]/.test(domain))
|
|
3843
|
+
throw new Error('SIWE domain must be an RFC-3986 authority');
|
|
3844
|
+
try {
|
|
3845
|
+
const parsedAuthority = new URL(`https://${domain}`);
|
|
3846
|
+
if (parsedAuthority.username || parsedAuthority.password || parsedAuthority.pathname !== '/' ||
|
|
3847
|
+
parsedAuthority.search || parsedAuthority.hash || parsedAuthority.host.toLowerCase() !== domain) {
|
|
3848
|
+
throw new Error('non-canonical authority');
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
catch (_f) {
|
|
3852
|
+
throw new Error('SIWE domain must be a canonical lowercase RFC-3986 authority');
|
|
3853
|
+
}
|
|
3854
|
+
let parsedUri;
|
|
3855
|
+
try {
|
|
3856
|
+
parsedUri = new URL(uri);
|
|
3857
|
+
}
|
|
3858
|
+
catch (_g) {
|
|
3859
|
+
throw new Error('SIWE URI must be an absolute URL');
|
|
3860
|
+
}
|
|
3861
|
+
const local = parsedUri.hostname === 'localhost' || parsedUri.hostname === '127.0.0.1' || parsedUri.hostname === '[::1]';
|
|
3862
|
+
if (parsedUri.protocol !== 'https:' && !(local && parsedUri.protocol === 'http:')) {
|
|
3863
|
+
throw new Error('SIWE URI must use HTTPS (HTTP is allowed only for localhost)');
|
|
3864
|
+
}
|
|
3865
|
+
if (parsedUri.username || parsedUri.password || parsedUri.hash || parsedUri.host.toLowerCase() !== domain.toLowerCase()) {
|
|
3866
|
+
throw new Error('SIWE URI authority must match the SIWE domain');
|
|
3867
|
+
}
|
|
3868
|
+
const issuedAtDate = (_d = options.issuedAt) !== null && _d !== void 0 ? _d : new Date();
|
|
3869
|
+
const expirationDate = (_e = options.expirationTime) !== null && _e !== void 0 ? _e : new Date(issuedAtDate.getTime() + DEFAULT_VALIDITY_MS);
|
|
3870
|
+
if (!Number.isFinite(issuedAtDate.getTime()) || !Number.isFinite(expirationDate.getTime()) || expirationDate <= issuedAtDate) {
|
|
3871
|
+
throw new Error('SIWE validity window is invalid');
|
|
3872
|
+
}
|
|
3873
|
+
if (expirationDate.getTime() - issuedAtDate.getTime() > 10 * 60 * 1000) {
|
|
3874
|
+
throw new Error('SIWE validity window exceeds 10 minutes');
|
|
3875
|
+
}
|
|
3876
|
+
const issuedAt = issuedAtDate.toISOString();
|
|
3877
|
+
const expirationTime = expirationDate.toISOString();
|
|
3878
|
+
return (`${domain} wants you to sign in with your Ethereum account:\n${addr}\n\n` +
|
|
3879
|
+
`${statement}\n\n` +
|
|
3880
|
+
`URI: ${parsedUri.toString()}\n` +
|
|
3881
|
+
`Version: ${version}\n` +
|
|
3882
|
+
`Chain ID: ${chainId}\n` +
|
|
3883
|
+
`Nonce: ${nonce}\n` +
|
|
3884
|
+
`Issued At: ${issuedAt}\n` +
|
|
3885
|
+
`Expiration Time: ${expirationTime}\n` +
|
|
3886
|
+
`Resources:\n` +
|
|
3887
|
+
`- bounded:app:${appId}`);
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Lowercase an EVM address to the canonical identity form. EVM addresses are
|
|
3891
|
+
* case-insensitive on-chain but the identity layer (@user.evmAddress) matches
|
|
3892
|
+
* case-sensitively, so every boundary normalizes to lowercase. Does not validate
|
|
3893
|
+
* shape — use normalizeEvmAddress() when the input is untrusted.
|
|
3894
|
+
*/
|
|
3895
|
+
function toLowerEvmAddress(address) {
|
|
3896
|
+
return address.trim().toLowerCase();
|
|
3897
|
+
}
|
|
3898
|
+
/**
|
|
3899
|
+
* Normalize an untrusted EVM address to canonical lowercase 0x-hex, or null when
|
|
3900
|
+
* it isn't a well-formed 20-byte hex address. Mirrors the realtime-worker's
|
|
3901
|
+
* normalizeEvmAddress (auth.ts) EXACTLY so a client-derived @user.evmAddress and
|
|
3902
|
+
* the server-authenticated one are byte-identical (fail-closed on malformed).
|
|
3903
|
+
*/
|
|
3904
|
+
function normalizeEvmAddress(raw) {
|
|
3905
|
+
if (typeof raw !== 'string')
|
|
3906
|
+
return null;
|
|
3907
|
+
const v = raw.trim().toLowerCase();
|
|
3908
|
+
return /^0x[0-9a-f]{40}$/.test(v) ? v : null;
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3749
3911
|
async function createBearerToken(isServer) {
|
|
3750
3912
|
if (isServer) {
|
|
3751
3913
|
const sessionMgr = ServerSessionManager.instance;
|
|
@@ -3794,7 +3956,7 @@ async function getUserInfo(isServer) {
|
|
|
3794
3956
|
* Returns all-null when the token can't be decoded (caller treats as no identity).
|
|
3795
3957
|
*/
|
|
3796
3958
|
function deriveUserIdentityFromIdToken(idToken) {
|
|
3797
|
-
const empty = { id: null, address: null, email: null };
|
|
3959
|
+
const empty = { id: null, address: null, email: null, evmAddress: null };
|
|
3798
3960
|
if (!idToken)
|
|
3799
3961
|
return empty;
|
|
3800
3962
|
try {
|
|
@@ -3807,14 +3969,25 @@ function deriveUserIdentityFromIdToken(idToken) {
|
|
|
3807
3969
|
? walletClaim
|
|
3808
3970
|
: null;
|
|
3809
3971
|
const userIdClaim = payload['custom:userId'];
|
|
3810
|
-
|
|
3972
|
+
let id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
|
|
3811
3973
|
? userIdClaim
|
|
3812
3974
|
: null;
|
|
3813
3975
|
const emailClaim = payload['email'];
|
|
3814
3976
|
const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
|
|
3815
3977
|
? emailClaim.toLowerCase()
|
|
3816
3978
|
: null;
|
|
3817
|
-
|
|
3979
|
+
// EVM wallet — carried on the `custom:evmWalletAddress` claim (an EVM login
|
|
3980
|
+
// OMITS custom:walletAddress). Normalized to canonical lowercase 0x-hex the
|
|
3981
|
+
// SAME way the realtime-worker does, so the client @user.evmAddress and the
|
|
3982
|
+
// server-authenticated one are byte-identical (fail-closed on malformed).
|
|
3983
|
+
const evmAddress = normalizeEvmAddress(payload['custom:evmWalletAddress']);
|
|
3984
|
+
// A SIWE identity uses its EVM address as custom:userId. Normalize legacy
|
|
3985
|
+
// mixed-case tokens at this boundary too so the principal and
|
|
3986
|
+
// @user.evmAddress cannot diverge solely by hex casing. Opaque human-login
|
|
3987
|
+
// ids remain byte-for-byte unchanged even when they have a linked wallet.
|
|
3988
|
+
if (evmAddress && id && normalizeEvmAddress(id) === evmAddress)
|
|
3989
|
+
id = evmAddress;
|
|
3990
|
+
return { id, address, email, evmAddress };
|
|
3818
3991
|
}
|
|
3819
3992
|
catch (_a) {
|
|
3820
3993
|
return empty;
|
|
@@ -4136,6 +4309,456 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
|
|
|
4136
4309
|
}
|
|
4137
4310
|
}
|
|
4138
4311
|
|
|
4312
|
+
/**
|
|
4313
|
+
* Selectors derived from the canonical BoundedRuntime signatures (BoundedTypes.sol):
|
|
4314
|
+
*
|
|
4315
|
+
* cast sig 'setDocuments(string,(string[],bool,(string,uint8,uint8,bytes)[])[])'
|
|
4316
|
+
* => 0x0827b094
|
|
4317
|
+
* cast sig 'setDocumentsAttested(string,(string[],bool,(string,uint8,uint8,bytes)[])[],(bytes32,uint256,uint256,uint8,bytes32,bytes32))'
|
|
4318
|
+
* => 0x87b111c4
|
|
4319
|
+
*
|
|
4320
|
+
* Keep the signatures beside the constants: changing either Solidity tuple is an
|
|
4321
|
+
* ABI break and must deliberately update these values and the Foundry goldens.
|
|
4322
|
+
*/
|
|
4323
|
+
const BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = '0x0827b094';
|
|
4324
|
+
const BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = '0x87b111c4';
|
|
4325
|
+
const SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR.slice(2);
|
|
4326
|
+
const SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR.slice(2);
|
|
4327
|
+
const MAX_CALLDATA_BYTES = 1024 * 1024;
|
|
4328
|
+
const MAX_ARRAY_ITEMS = 2048;
|
|
4329
|
+
const BIGINT_ZERO = BigInt(0);
|
|
4330
|
+
const BIGINT_ONE = BigInt(1);
|
|
4331
|
+
const BIGINT_EIGHT = BigInt(8);
|
|
4332
|
+
const SECP256K1_N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
|
|
4333
|
+
const SECP256K1_HALF_N = BigInt('0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0');
|
|
4334
|
+
function hexBytes(raw) {
|
|
4335
|
+
if (typeof raw !== 'string' || !/^0x[0-9a-fA-F]*$/.test(raw) || raw.length % 2 !== 0) {
|
|
4336
|
+
throw new Error('EVM transaction calldata must be even-length 0x hex');
|
|
4337
|
+
}
|
|
4338
|
+
const hex = raw.slice(2);
|
|
4339
|
+
if (hex.length / 2 > MAX_CALLDATA_BYTES)
|
|
4340
|
+
throw new Error('EVM transaction calldata exceeds the client safety limit');
|
|
4341
|
+
const out = new Uint8Array(hex.length / 2);
|
|
4342
|
+
for (let i = 0; i < out.length; i++)
|
|
4343
|
+
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
4344
|
+
return out;
|
|
4345
|
+
}
|
|
4346
|
+
class AbiReader {
|
|
4347
|
+
constructor(bytes) {
|
|
4348
|
+
this.bytes = bytes;
|
|
4349
|
+
this.maxRead = 0;
|
|
4350
|
+
this.dynamicPayloadBytes = 0;
|
|
4351
|
+
this.utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: false });
|
|
4352
|
+
}
|
|
4353
|
+
range(offset, length, label) {
|
|
4354
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > this.bytes.length) {
|
|
4355
|
+
throw new Error(`Malformed EVM setDocuments calldata (${label})`);
|
|
4356
|
+
}
|
|
4357
|
+
this.maxRead = Math.max(this.maxRead, offset + length);
|
|
4358
|
+
}
|
|
4359
|
+
word(offset, label) {
|
|
4360
|
+
this.range(offset, 32, label);
|
|
4361
|
+
let value = BIGINT_ZERO;
|
|
4362
|
+
for (let i = 0; i < 32; i++)
|
|
4363
|
+
value = (value << BIGINT_EIGHT) | BigInt(this.bytes[offset + i]);
|
|
4364
|
+
return value;
|
|
4365
|
+
}
|
|
4366
|
+
numberWord(offset, label, max = Number.MAX_SAFE_INTEGER) {
|
|
4367
|
+
const value = this.word(offset, label);
|
|
4368
|
+
if (value > BigInt(max))
|
|
4369
|
+
throw new Error(`Malformed EVM setDocuments calldata (${label} too large)`);
|
|
4370
|
+
return Number(value);
|
|
4371
|
+
}
|
|
4372
|
+
offset(base, wordOffset, min, label) {
|
|
4373
|
+
const relative = this.numberWord(wordOffset, `${label} offset`, this.bytes.length);
|
|
4374
|
+
if (relative < min || relative % 32 !== 0)
|
|
4375
|
+
throw new Error(`Malformed EVM setDocuments calldata (${label} offset)`);
|
|
4376
|
+
const absolute = base + relative;
|
|
4377
|
+
this.range(absolute, 0, label);
|
|
4378
|
+
return absolute;
|
|
4379
|
+
}
|
|
4380
|
+
dynamicBytes(at, label) {
|
|
4381
|
+
const length = this.numberWord(at, `${label} length`, MAX_CALLDATA_BYTES);
|
|
4382
|
+
this.dynamicPayloadBytes += length;
|
|
4383
|
+
// ABI offsets may alias. Cap the cumulative decoded payload so a malicious
|
|
4384
|
+
// 1 MB tail referenced by thousands of fields cannot amplify into a
|
|
4385
|
+
// multi-gigabyte allocation/UTF-8 decode in the wallet process.
|
|
4386
|
+
if (this.dynamicPayloadBytes > MAX_CALLDATA_BYTES) {
|
|
4387
|
+
throw new Error('Malformed EVM setDocuments calldata (dynamic payload amplification)');
|
|
4388
|
+
}
|
|
4389
|
+
const start = at + 32;
|
|
4390
|
+
const padded = Math.ceil(length / 32) * 32;
|
|
4391
|
+
this.range(start, padded, label);
|
|
4392
|
+
for (let i = start + length; i < start + padded; i++) {
|
|
4393
|
+
if (this.bytes[i] !== 0)
|
|
4394
|
+
throw new Error(`Malformed EVM setDocuments calldata (${label} padding)`);
|
|
4395
|
+
}
|
|
4396
|
+
return this.bytes.slice(start, start + length);
|
|
4397
|
+
}
|
|
4398
|
+
dynamicEnd(at, label) {
|
|
4399
|
+
const length = this.numberWord(at, `${label} length`, MAX_CALLDATA_BYTES);
|
|
4400
|
+
const end = at + 32 + Math.ceil(length / 32) * 32;
|
|
4401
|
+
this.range(at + 32, Math.ceil(length / 32) * 32, label);
|
|
4402
|
+
return end;
|
|
4403
|
+
}
|
|
4404
|
+
string(at, label) {
|
|
4405
|
+
try {
|
|
4406
|
+
return this.utf8.decode(this.dynamicBytes(at, label));
|
|
4407
|
+
}
|
|
4408
|
+
catch (_a) {
|
|
4409
|
+
throw new Error(`Malformed EVM setDocuments calldata (${label} utf8)`);
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
finish() {
|
|
4413
|
+
if (this.maxRead !== this.bytes.length)
|
|
4414
|
+
throw new Error('Malformed EVM setDocuments calldata (unreferenced trailing bytes)');
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
function assertCanonicalOffset(actual, expected, label) {
|
|
4418
|
+
if (actual !== expected)
|
|
4419
|
+
throw new Error(`Malformed EVM setDocuments calldata (${label} non-canonical offset)`);
|
|
4420
|
+
}
|
|
4421
|
+
function decodeStringArray(reader, at, label) {
|
|
4422
|
+
const count = reader.numberWord(at, `${label} count`, MAX_ARRAY_ITEMS);
|
|
4423
|
+
const head = at + 32;
|
|
4424
|
+
const min = count * 32;
|
|
4425
|
+
const out = [];
|
|
4426
|
+
let expectedTail = head + min;
|
|
4427
|
+
for (let i = 0; i < count; i++) {
|
|
4428
|
+
const itemAt = reader.offset(head, head + i * 32, min, `${label}[${i}]`);
|
|
4429
|
+
assertCanonicalOffset(itemAt, expectedTail, `${label}[${i}]`);
|
|
4430
|
+
out.push(reader.string(itemAt, `${label}[${i}]`));
|
|
4431
|
+
expectedTail = reader.dynamicEnd(itemAt, `${label}[${i}]`);
|
|
4432
|
+
}
|
|
4433
|
+
return { value: out, end: expectedTail };
|
|
4434
|
+
}
|
|
4435
|
+
function decodeFields(reader, at) {
|
|
4436
|
+
const count = reader.numberWord(at, 'fields count', MAX_ARRAY_ITEMS);
|
|
4437
|
+
const head = at + 32;
|
|
4438
|
+
const min = count * 32;
|
|
4439
|
+
const out = [];
|
|
4440
|
+
let expectedTail = head + min;
|
|
4441
|
+
for (let i = 0; i < count; i++) {
|
|
4442
|
+
const tuple = reader.offset(head, head + i * 32, min, `field[${i}]`);
|
|
4443
|
+
assertCanonicalOffset(tuple, expectedTail, `field[${i}]`);
|
|
4444
|
+
const nameAt = reader.offset(tuple, tuple, 128, `field[${i}].name`);
|
|
4445
|
+
assertCanonicalOffset(nameAt, tuple + 128, `field[${i}].name`);
|
|
4446
|
+
const name = reader.string(nameAt, `field[${i}].name`);
|
|
4447
|
+
const nameEnd = reader.dynamicEnd(nameAt, `field[${i}].name`);
|
|
4448
|
+
const ftype = reader.numberWord(tuple + 32, `field[${i}].ftype`, 255);
|
|
4449
|
+
const op = reader.numberWord(tuple + 64, `field[${i}].op`, 255);
|
|
4450
|
+
const valueAt = reader.offset(tuple, tuple + 96, 128, `field[${i}].value`);
|
|
4451
|
+
assertCanonicalOffset(valueAt, nameEnd, `field[${i}].value`);
|
|
4452
|
+
const value = reader.dynamicBytes(valueAt, `field[${i}].value`);
|
|
4453
|
+
expectedTail = reader.dynamicEnd(valueAt, `field[${i}].value`);
|
|
4454
|
+
out.push({ name, ftype, op, value });
|
|
4455
|
+
}
|
|
4456
|
+
return { value: out, end: expectedTail };
|
|
4457
|
+
}
|
|
4458
|
+
function decodeRuntimeWrite(rawData) {
|
|
4459
|
+
const bytes = hexBytes(rawData);
|
|
4460
|
+
if (bytes.length < 68)
|
|
4461
|
+
throw new Error('Malformed EVM setDocuments calldata');
|
|
4462
|
+
const selector = Array.from(bytes.slice(0, 4)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
4463
|
+
const kind = selector === SET_DOCUMENTS_SELECTOR
|
|
4464
|
+
? 'setDocuments'
|
|
4465
|
+
: selector === SET_DOCUMENTS_ATTESTED_SELECTOR
|
|
4466
|
+
? 'setDocumentsAttested'
|
|
4467
|
+
: null;
|
|
4468
|
+
if (!kind)
|
|
4469
|
+
throw new Error('EVM transaction is not a supported BoundedRuntime document write');
|
|
4470
|
+
const reader = new AbiReader(bytes);
|
|
4471
|
+
reader.maxRead = 4;
|
|
4472
|
+
const args = 4;
|
|
4473
|
+
// ExternalCallAttestation is a fully-static six-word tuple and is therefore
|
|
4474
|
+
// expanded inline into the top-level head: 2 dynamic offsets + 6 words.
|
|
4475
|
+
const headBytes = kind === 'setDocumentsAttested' ? 256 : 64;
|
|
4476
|
+
const appAt = reader.offset(args, args, headBytes, 'appId');
|
|
4477
|
+
assertCanonicalOffset(appAt, args + headBytes, 'appId');
|
|
4478
|
+
const opsAt = reader.offset(args, args + 32, headBytes, 'ops');
|
|
4479
|
+
const appId = reader.string(appAt, 'appId');
|
|
4480
|
+
const appEnd = reader.dynamicEnd(appAt, 'appId');
|
|
4481
|
+
assertCanonicalOffset(opsAt, appEnd, 'ops');
|
|
4482
|
+
const count = reader.numberWord(opsAt, 'ops count', MAX_ARRAY_ITEMS);
|
|
4483
|
+
const head = opsAt + 32;
|
|
4484
|
+
const min = count * 32;
|
|
4485
|
+
const ops = [];
|
|
4486
|
+
let expectedTail = head + min;
|
|
4487
|
+
for (let i = 0; i < count; i++) {
|
|
4488
|
+
const tuple = reader.offset(head, head + i * 32, min, `op[${i}]`);
|
|
4489
|
+
assertCanonicalOffset(tuple, expectedTail, `op[${i}]`);
|
|
4490
|
+
const pathAt = reader.offset(tuple, tuple, 96, `op[${i}].path`);
|
|
4491
|
+
assertCanonicalOffset(pathAt, tuple + 96, `op[${i}].path`);
|
|
4492
|
+
const deleteWord = reader.numberWord(tuple + 32, `op[${i}].isDelete`, 1);
|
|
4493
|
+
const fieldsAt = reader.offset(tuple, tuple + 64, 96, `op[${i}].fields`);
|
|
4494
|
+
const path = decodeStringArray(reader, pathAt, `op[${i}].path`);
|
|
4495
|
+
assertCanonicalOffset(fieldsAt, path.end, `op[${i}].fields`);
|
|
4496
|
+
const fields = decodeFields(reader, fieldsAt);
|
|
4497
|
+
expectedTail = fields.end;
|
|
4498
|
+
ops.push({
|
|
4499
|
+
pathSegments: path.value,
|
|
4500
|
+
isDelete: deleteWord === 1,
|
|
4501
|
+
fields: fields.value,
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
4504
|
+
let attestation;
|
|
4505
|
+
if (kind === 'setDocumentsAttested') {
|
|
4506
|
+
const callsHash = reader.word(args + 64, 'attestation.callsHash');
|
|
4507
|
+
const nonce = reader.word(args + 96, 'attestation.nonce');
|
|
4508
|
+
const deadline = reader.word(args + 128, 'attestation.deadline');
|
|
4509
|
+
const v = reader.numberWord(args + 160, 'attestation.v', 255);
|
|
4510
|
+
const r = reader.word(args + 192, 'attestation.r');
|
|
4511
|
+
const s = reader.word(args + 224, 'attestation.s');
|
|
4512
|
+
if (v !== 27 && v !== 28)
|
|
4513
|
+
throw new Error('Malformed EVM setDocuments calldata (attestation.v must be 27 or 28)');
|
|
4514
|
+
if (r === BIGINT_ZERO || r >= SECP256K1_N)
|
|
4515
|
+
throw new Error('Malformed EVM setDocuments calldata (attestation.r is not canonical)');
|
|
4516
|
+
if (s === BIGINT_ZERO || s > SECP256K1_HALF_N)
|
|
4517
|
+
throw new Error('Malformed EVM setDocuments calldata (attestation.s is not canonical low-s)');
|
|
4518
|
+
attestation = { callsHash, nonce, deadline, v, r, s };
|
|
4519
|
+
}
|
|
4520
|
+
if (expectedTail !== bytes.length)
|
|
4521
|
+
throw new Error('Malformed EVM setDocuments calldata (unreferenced trailing bytes)');
|
|
4522
|
+
reader.finish();
|
|
4523
|
+
return Object.assign({ kind, appId, ops }, (attestation ? { attestation } : {}));
|
|
4524
|
+
}
|
|
4525
|
+
/**
|
|
4526
|
+
* Fail closed unless calldata is a byte-for-byte canonical ABI encoding of one
|
|
4527
|
+
* supported BoundedRuntime write. This is the defense used by direct signer
|
|
4528
|
+
* surfaces; the higher-level write validator below additionally binds app/ops.
|
|
4529
|
+
*/
|
|
4530
|
+
function assertCanonicalBoundedRuntimeWriteCalldata(rawData) {
|
|
4531
|
+
decodeRuntimeWrite(rawData);
|
|
4532
|
+
}
|
|
4533
|
+
const MAX_EVM_DOCUMENT_PATH_UTF8_BYTES = 1024;
|
|
4534
|
+
function canonicalUtf8ByteLength(value) {
|
|
4535
|
+
let bytes = 0;
|
|
4536
|
+
for (let i = 0; i < value.length; i++) {
|
|
4537
|
+
const unit = value.charCodeAt(i);
|
|
4538
|
+
if (unit <= 0x7f)
|
|
4539
|
+
bytes += 1;
|
|
4540
|
+
else if (unit <= 0x7ff)
|
|
4541
|
+
bytes += 2;
|
|
4542
|
+
else if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
4543
|
+
const low = value.charCodeAt(++i);
|
|
4544
|
+
if (!(low >= 0xdc00 && low <= 0xdfff))
|
|
4545
|
+
throw new Error(`Invalid EVM write path: ${value}`);
|
|
4546
|
+
bytes += 4;
|
|
4547
|
+
}
|
|
4548
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
4549
|
+
throw new Error(`Invalid EVM write path: ${value}`);
|
|
4550
|
+
}
|
|
4551
|
+
else
|
|
4552
|
+
bytes += 3;
|
|
4553
|
+
}
|
|
4554
|
+
return bytes;
|
|
4555
|
+
}
|
|
4556
|
+
function normalizedPathSegments(path) {
|
|
4557
|
+
if (typeof path !== 'string' || path.length === 0 ||
|
|
4558
|
+
canonicalUtf8ByteLength(path) > MAX_EVM_DOCUMENT_PATH_UTF8_BYTES) {
|
|
4559
|
+
throw new Error(`Invalid EVM write path: ${path}`);
|
|
4560
|
+
}
|
|
4561
|
+
const segments = path.split('/');
|
|
4562
|
+
if (segments.length < 2 || segments.length % 2 !== 0 ||
|
|
4563
|
+
segments[0].startsWith('__') ||
|
|
4564
|
+
segments.some((s) => !s || s === '.' || s === '..' || /[\\\u0000-\u001f\u007f]/.test(s))) {
|
|
4565
|
+
throw new Error(`Invalid EVM write path: ${path}`);
|
|
4566
|
+
}
|
|
4567
|
+
return segments;
|
|
4568
|
+
}
|
|
4569
|
+
function bytesBigInt(bytes, signed) {
|
|
4570
|
+
if (bytes.length !== 32)
|
|
4571
|
+
throw new Error('EVM field value is not one ABI word');
|
|
4572
|
+
let value = BIGINT_ZERO;
|
|
4573
|
+
for (const b of bytes)
|
|
4574
|
+
value = (value << BIGINT_EIGHT) | BigInt(b);
|
|
4575
|
+
if (signed && (value & (BIGINT_ONE << BigInt(255))) !== BIGINT_ZERO)
|
|
4576
|
+
value -= BIGINT_ONE << BigInt(256);
|
|
4577
|
+
return value;
|
|
4578
|
+
}
|
|
4579
|
+
function decodedAbiString(bytes) {
|
|
4580
|
+
const reader = new AbiReader(bytes);
|
|
4581
|
+
const at = reader.offset(0, 0, 32, 'field string');
|
|
4582
|
+
const value = reader.string(at, 'field string');
|
|
4583
|
+
reader.finish();
|
|
4584
|
+
return value;
|
|
4585
|
+
}
|
|
4586
|
+
function assertSetValue(field, expected) {
|
|
4587
|
+
if (field.op !== 0)
|
|
4588
|
+
throw new Error(`EVM field ${field.name} changes the requested SET action`);
|
|
4589
|
+
if (typeof expected === 'boolean') {
|
|
4590
|
+
if (field.ftype !== 3 || bytesBigInt(field.value, false) !== (expected ? BIGINT_ONE : BIGINT_ZERO))
|
|
4591
|
+
throw new Error(`EVM field ${field.name} value mismatch`);
|
|
4592
|
+
return;
|
|
4593
|
+
}
|
|
4594
|
+
if (typeof expected === 'number') {
|
|
4595
|
+
if (!Number.isFinite(expected))
|
|
4596
|
+
throw new Error(`EVM field ${field.name} is not finite`);
|
|
4597
|
+
if (Number.isInteger(expected)) {
|
|
4598
|
+
if (!Number.isSafeInteger(expected)) {
|
|
4599
|
+
throw new Error(`EVM field ${field.name} integer must be a safe number or canonical decimal string`);
|
|
4600
|
+
}
|
|
4601
|
+
const actual = field.ftype === 1 ? bytesBigInt(field.value, false) : field.ftype === 2 ? bytesBigInt(field.value, true) : null;
|
|
4602
|
+
if (actual === null || actual !== BigInt(expected))
|
|
4603
|
+
throw new Error(`EVM field ${field.name} value mismatch`);
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
if (field.ftype !== 4 || decodedAbiString(field.value) !== expected.toString())
|
|
4607
|
+
throw new Error(`EVM field ${field.name} value mismatch`);
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4610
|
+
if (typeof expected === 'bigint') {
|
|
4611
|
+
const actual = field.ftype === 1 ? bytesBigInt(field.value, false) : field.ftype === 2 ? bytesBigInt(field.value, true) : null;
|
|
4612
|
+
if (actual === null || actual !== expected)
|
|
4613
|
+
throw new Error(`EVM field ${field.name} value mismatch`);
|
|
4614
|
+
return;
|
|
4615
|
+
}
|
|
4616
|
+
if (typeof expected === 'string') {
|
|
4617
|
+
if (field.ftype === 4 && decodedAbiString(field.value) === expected)
|
|
4618
|
+
return;
|
|
4619
|
+
if ((field.ftype === 1 || field.ftype === 2) && expected.length <= 20 && /^-?(?:0|[1-9][0-9]*)$/.test(expected)) {
|
|
4620
|
+
const exact = BigInt(expected);
|
|
4621
|
+
if (exact >= BigInt(Number.MIN_SAFE_INTEGER) && exact <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
4622
|
+
throw new Error(`EVM field ${field.name} safe integer must be supplied as a number`);
|
|
4623
|
+
}
|
|
4624
|
+
const inRange = field.ftype === 1
|
|
4625
|
+
? exact >= BIGINT_ZERO && exact <= ((BIGINT_ONE << BigInt(64)) - BIGINT_ONE)
|
|
4626
|
+
: exact >= -(BIGINT_ONE << BigInt(63)) && exact <= ((BIGINT_ONE << BigInt(63)) - BIGINT_ONE);
|
|
4627
|
+
const actual = bytesBigInt(field.value, field.ftype === 2);
|
|
4628
|
+
if (inRange && actual === exact)
|
|
4629
|
+
return;
|
|
4630
|
+
throw new Error(`EVM field ${field.name} value mismatch`);
|
|
4631
|
+
}
|
|
4632
|
+
if (field.ftype === 5 && normalizeEvmAddress(expected)) {
|
|
4633
|
+
const word = field.value;
|
|
4634
|
+
if (word.length === 32 && word.slice(0, 12).every((b) => b === 0)) {
|
|
4635
|
+
const actual = `0x${Array.from(word.slice(12)).map((b) => b.toString(16).padStart(2, '0')).join('')}`;
|
|
4636
|
+
if (actual === normalizeEvmAddress(expected))
|
|
4637
|
+
return;
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
throw new Error(`EVM field ${field.name} value mismatch`);
|
|
4641
|
+
}
|
|
4642
|
+
if (expected === undefined || expected === null) {
|
|
4643
|
+
throw new Error(`EVM field ${field.name} cannot be represented safely on EVM; use a document delete or omit the field`);
|
|
4644
|
+
}
|
|
4645
|
+
const json = JSON.stringify(expected);
|
|
4646
|
+
if (typeof json !== 'string' || field.ftype !== 4 || decodedAbiString(field.value) !== json) {
|
|
4647
|
+
throw new Error(`EVM field ${field.name} value mismatch`);
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
function assertField(field, expected) {
|
|
4651
|
+
if (expected === null) {
|
|
4652
|
+
if (field.op !== 2 || field.ftype < 1 || field.ftype > 5 || field.value.length !== 0) {
|
|
4653
|
+
throw new Error(`EVM field ${field.name} deletion mismatch`);
|
|
4654
|
+
}
|
|
4655
|
+
return;
|
|
4656
|
+
}
|
|
4657
|
+
if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
|
|
4658
|
+
const operation = expected.operation;
|
|
4659
|
+
if (operation === 'increment') {
|
|
4660
|
+
const delta = expected.value;
|
|
4661
|
+
const actual = field.ftype === 1 || field.ftype === 2
|
|
4662
|
+
? bytesBigInt(field.value, true)
|
|
4663
|
+
: null;
|
|
4664
|
+
if (!Number.isSafeInteger(delta) || field.op !== 3 || actual === null || actual !== BigInt(delta)) {
|
|
4665
|
+
throw new Error(`EVM field ${field.name} increment mismatch`);
|
|
4666
|
+
}
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
if (operation === 'time' && expected.value === 'now') {
|
|
4670
|
+
if (field.op !== 4 || (field.ftype !== 1 && field.ftype !== 2) || field.value.length !== 0) {
|
|
4671
|
+
throw new Error(`EVM field ${field.name} timestamp mismatch`);
|
|
4672
|
+
}
|
|
4673
|
+
return;
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
assertSetValue(field, expected);
|
|
4677
|
+
}
|
|
4678
|
+
function assertOp(decoded, path, document) {
|
|
4679
|
+
const expectedSegments = normalizedPathSegments(path);
|
|
4680
|
+
if (decoded.pathSegments.length !== expectedSegments.length || decoded.pathSegments.some((s, i) => s !== expectedSegments[i])) {
|
|
4681
|
+
throw new Error(`EVM transaction path does not match requested write: ${path}`);
|
|
4682
|
+
}
|
|
4683
|
+
if (decoded.isDelete !== (document === null))
|
|
4684
|
+
throw new Error(`EVM transaction action does not match requested write: ${path}`);
|
|
4685
|
+
if (document === null) {
|
|
4686
|
+
if (decoded.fields.length !== 0)
|
|
4687
|
+
throw new Error(`EVM delete unexpectedly carries fields: ${path}`);
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
if (!document || typeof document !== 'object' || Array.isArray(document))
|
|
4691
|
+
throw new Error(`EVM document must be a plain object or null: ${path}`);
|
|
4692
|
+
const expectedEntries = Object.entries(document);
|
|
4693
|
+
if (expectedEntries.some(([name]) => name.startsWith('_')))
|
|
4694
|
+
throw new Error(`Reserved EVM field reached transaction validation: ${path}`);
|
|
4695
|
+
const expected = new Map(expectedEntries);
|
|
4696
|
+
const seen = new Set();
|
|
4697
|
+
for (const field of decoded.fields) {
|
|
4698
|
+
if (!field.name || seen.has(field.name) || !expected.has(field.name))
|
|
4699
|
+
throw new Error(`EVM transaction contains unexpected or duplicate field ${field.name}`);
|
|
4700
|
+
seen.add(field.name);
|
|
4701
|
+
assertField(field, expected.get(field.name));
|
|
4702
|
+
}
|
|
4703
|
+
const missing = [...expected.keys()].filter((name) => !seen.has(name));
|
|
4704
|
+
if (missing.length > 0)
|
|
4705
|
+
throw new Error(`EVM transaction omits requested fields: ${missing.join(', ')}`);
|
|
4706
|
+
}
|
|
4707
|
+
function zeroWei(raw) {
|
|
4708
|
+
if (raw === undefined || raw === null || raw === '')
|
|
4709
|
+
return true;
|
|
4710
|
+
if (typeof raw !== 'string' && typeof raw !== 'number')
|
|
4711
|
+
return false;
|
|
4712
|
+
try {
|
|
4713
|
+
return BigInt(raw) === BIGINT_ZERO;
|
|
4714
|
+
}
|
|
4715
|
+
catch (_a) {
|
|
4716
|
+
return false;
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
/** Verify the untrusted 202 payload is exactly the governed write the app asked for. */
|
|
4720
|
+
function assertEvmTransactionMatchesWrite(transaction, context) {
|
|
4721
|
+
var _a, _b;
|
|
4722
|
+
if (!transaction || !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0) {
|
|
4723
|
+
throw new Error('EVM transaction chainId is required');
|
|
4724
|
+
}
|
|
4725
|
+
const chainId = transaction.chainId;
|
|
4726
|
+
const policy = context.policy;
|
|
4727
|
+
if (!policy)
|
|
4728
|
+
throw new Error('EVM transaction signing requires an explicit runtime/chain policy');
|
|
4729
|
+
if (policy.chainId !== undefined && policy.chainId !== chainId)
|
|
4730
|
+
throw new Error(`EVM transaction chain ${chainId} does not match configured chain ${policy.chainId}`);
|
|
4731
|
+
const expectedRuntime = (_b = (_a = policy.runtimeAddresses) === null || _a === void 0 ? void 0 : _a[String(chainId)]) !== null && _b !== void 0 ? _b : (policy.chainId === chainId ? policy.runtimeAddress : undefined);
|
|
4732
|
+
const normalizedRuntime = normalizeEvmAddress(expectedRuntime);
|
|
4733
|
+
if (!normalizedRuntime)
|
|
4734
|
+
throw new Error(`No trusted BoundedRuntime address configured for EVM chain ${chainId}`);
|
|
4735
|
+
if (normalizeEvmAddress(transaction.to) !== normalizedRuntime)
|
|
4736
|
+
throw new Error('EVM transaction target is not the configured BoundedRuntime');
|
|
4737
|
+
if (!zeroWei(transaction.value))
|
|
4738
|
+
throw new Error('BoundedRuntime.setDocuments must not transfer native value');
|
|
4739
|
+
if (transaction.nonce !== undefined)
|
|
4740
|
+
throw new Error('Server-supplied EVM nonce is not accepted');
|
|
4741
|
+
const decoded = decodeRuntimeWrite(transaction.data);
|
|
4742
|
+
if (decoded.appId !== context.appId)
|
|
4743
|
+
throw new Error('EVM transaction appId does not match configured appId');
|
|
4744
|
+
if (decoded.ops.length !== context.documents.length)
|
|
4745
|
+
throw new Error('EVM transaction operation count does not match requested writes');
|
|
4746
|
+
// The worker's contract ABI groups upserts first and document deletes second.
|
|
4747
|
+
const expectedOps = [
|
|
4748
|
+
...context.documents.filter((entry) => entry.document !== null),
|
|
4749
|
+
...context.documents.filter((entry) => entry.document === null),
|
|
4750
|
+
];
|
|
4751
|
+
const paths = new Set();
|
|
4752
|
+
for (let i = 0; i < expectedOps.length; i++) {
|
|
4753
|
+
const expected = expectedOps[i];
|
|
4754
|
+
const canonicalPath = normalizedPathSegments(expected.destinationPath).join('/');
|
|
4755
|
+
if (paths.has(canonicalPath))
|
|
4756
|
+
throw new Error(`Duplicate EVM write path is not allowed: ${canonicalPath}`);
|
|
4757
|
+
paths.add(canonicalPath);
|
|
4758
|
+
assertOp(decoded.ops[i], canonicalPath, expected.document);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4139
4762
|
var __rest$1 = (undefined && undefined.__rest) || function (s, e) {
|
|
4140
4763
|
var t = {};
|
|
4141
4764
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
@@ -4272,6 +4895,21 @@ function hashForKey$1(value) {
|
|
|
4272
4895
|
}
|
|
4273
4896
|
return h.toString(36);
|
|
4274
4897
|
}
|
|
4898
|
+
// Memoize the compact principal hash per exact credential string. Every read
|
|
4899
|
+
// hashed the entire (~1KB) bearer/idToken on every call; a session presents
|
|
4900
|
+
// the same handful of credentials for its lifetime. Bounded size — a rotation
|
|
4901
|
+
// simply inserts the new token and stale entries fall off on clear.
|
|
4902
|
+
const principalHashCache = new Map();
|
|
4903
|
+
function hashForKeyCached(value) {
|
|
4904
|
+
const hit = principalHashCache.get(value);
|
|
4905
|
+
if (hit !== undefined)
|
|
4906
|
+
return hit;
|
|
4907
|
+
const h = hashForKey$1(value);
|
|
4908
|
+
if (principalHashCache.size >= 50)
|
|
4909
|
+
principalHashCache.clear();
|
|
4910
|
+
principalHashCache.set(value, h);
|
|
4911
|
+
return h;
|
|
4912
|
+
}
|
|
4275
4913
|
function hasAuthHeader(headers) {
|
|
4276
4914
|
if (!headers)
|
|
4277
4915
|
return false;
|
|
@@ -4282,10 +4920,10 @@ function authValueFromHeaders(headers) {
|
|
|
4282
4920
|
return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
|
|
4283
4921
|
}
|
|
4284
4922
|
function principalFromAuthValue(authValue) {
|
|
4285
|
-
return authValue ? `h${
|
|
4923
|
+
return authValue ? `h${hashForKeyCached(authValue)}` : null;
|
|
4286
4924
|
}
|
|
4287
4925
|
function principalFromIdToken$1(idToken) {
|
|
4288
|
-
return idToken ? `t${
|
|
4926
|
+
return idToken ? `t${hashForKeyCached(idToken)}` : null;
|
|
4289
4927
|
}
|
|
4290
4928
|
function uncacheableReadKey(appId, scope) {
|
|
4291
4929
|
uncacheableReadKeyCounter += 1;
|
|
@@ -5166,13 +5804,12 @@ async function runExpressionMany(many) {
|
|
|
5166
5804
|
* collection's policy `delete` rule and broadcasts a delete to subscribers.
|
|
5167
5805
|
*/
|
|
5168
5806
|
async function set(path, document, options) {
|
|
5169
|
-
|
|
5170
|
-
//
|
|
5171
|
-
|
|
5172
|
-
return result;
|
|
5807
|
+
// setMany already invalidates the read cache for the affected document and
|
|
5808
|
+
// collection paths — no second sweep needed here.
|
|
5809
|
+
return setMany([{ path, document }], options);
|
|
5173
5810
|
}
|
|
5174
5811
|
async function setMany(many, options) {
|
|
5175
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
5812
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
5176
5813
|
// Returns the data that was set, or undefined if the document was already set.
|
|
5177
5814
|
try {
|
|
5178
5815
|
const config = await getConfig();
|
|
@@ -5228,6 +5865,9 @@ async function setMany(many, options) {
|
|
|
5228
5865
|
// the API did nothing and gave us the required info to run this transaction.
|
|
5229
5866
|
const { transactions, offchainTransaction } = setResponse.data;
|
|
5230
5867
|
// Handle offchain transaction flow (for 'offchain' protocol apps)
|
|
5868
|
+
if (offchainTransaction && Array.isArray(transactions) && transactions.length > 0) {
|
|
5869
|
+
throw new Error('Ambiguous write response mixes offchain and onchain transaction targets');
|
|
5870
|
+
}
|
|
5231
5871
|
if (offchainTransaction) {
|
|
5232
5872
|
const transactionResult = await handleOffchainTransaction(offchainTransaction, authProvider, options);
|
|
5233
5873
|
return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: transactionResult.signature, signedTransaction: transactionResult.signedTransaction });
|
|
@@ -5237,9 +5877,35 @@ async function setMany(many, options) {
|
|
|
5237
5877
|
throw new Error(`Expected exactly one on-chain transaction, received ${Array.isArray(transactions) ? transactions.length : 0}`);
|
|
5238
5878
|
}
|
|
5239
5879
|
const curTx = transactions[0];
|
|
5880
|
+
const hasEvmTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.evmTransaction) != null;
|
|
5881
|
+
const hasSerializedSolanaTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.serializedTransaction) != null;
|
|
5882
|
+
const hasBuiltSolanaTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.transactionArgs) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.remainingAccounts) != null ||
|
|
5883
|
+
(curTx === null || curTx === void 0 ? void 0 : curTx.idl) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.txData) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.preInstructions) != null;
|
|
5884
|
+
if ((hasEvmTarget && (hasSerializedSolanaTarget || hasBuiltSolanaTarget)) ||
|
|
5885
|
+
(hasSerializedSolanaTarget && hasBuiltSolanaTarget)) {
|
|
5886
|
+
throw new Error('Ambiguous on-chain write response mixes EVM and/or Solana transaction target formats');
|
|
5887
|
+
}
|
|
5240
5888
|
let transactionResult;
|
|
5241
5889
|
const expectedWritePaths = documents.map(d => d.destinationPath);
|
|
5242
|
-
|
|
5890
|
+
// EVM onchain write: the worker returns a PRE-BUILT EIP-1559 tx on
|
|
5891
|
+
// `transactions[0].evmTransaction` (the EVM twin of Solana's pre-built
|
|
5892
|
+
// `serializedTransaction`). WORKER 202 CONTRACT (field name + shape the
|
|
5893
|
+
// worker side must emit to match):
|
|
5894
|
+
// { transactions: [ { evmTransaction: {
|
|
5895
|
+
// to, data, value?, chainId, gas?, maxFeePerGas?, maxPriorityFeePerGas?, nonce?
|
|
5896
|
+
// } } ] }
|
|
5897
|
+
// We hand it to the EVM auth provider's runTransaction(evmTx, undefined, options)
|
|
5898
|
+
// (eth_sendTransaction — the wallet signs + submits). syncItems below is
|
|
5899
|
+
// identical to the Solana path.
|
|
5900
|
+
if (curTx.evmTransaction) {
|
|
5901
|
+
assertEvmTransactionMatchesWrite(curTx.evmTransaction, {
|
|
5902
|
+
appId: config.appId,
|
|
5903
|
+
documents,
|
|
5904
|
+
policy: (_k = (_j = (_h = options === null || options === void 0 ? void 0 : options._overrides) === null || _h === void 0 ? void 0 : _h._evmTransactionPolicy) !== null && _j !== void 0 ? _j : config.evmWalletConfig) !== null && _k !== void 0 ? _k : null,
|
|
5905
|
+
});
|
|
5906
|
+
transactionResult = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
|
|
5907
|
+
}
|
|
5908
|
+
else if (curTx.serializedTransaction) {
|
|
5243
5909
|
transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
|
|
5244
5910
|
}
|
|
5245
5911
|
else {
|
|
@@ -5263,7 +5929,7 @@ async function setMany(many, options) {
|
|
|
5263
5929
|
else if (setResponse.data &&
|
|
5264
5930
|
typeof setResponse.data === 'object' &&
|
|
5265
5931
|
setResponse.data.success === true) {
|
|
5266
|
-
const
|
|
5932
|
+
const _l = setResponse.data, { success: _success } = _l, rest = __rest$1(_l, ["success"]);
|
|
5267
5933
|
return Object.assign(Object.assign(Object.assign({}, documents.map(d => d.document)), rest), { transactionId: null });
|
|
5268
5934
|
}
|
|
5269
5935
|
else {
|
|
@@ -5363,6 +6029,14 @@ async function setMany(many, options) {
|
|
|
5363
6029
|
const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
|
|
5364
6030
|
return transactionResult;
|
|
5365
6031
|
}
|
|
6032
|
+
// EVM onchain write: hand the worker's pre-built EIP-1559 tx to the EVM auth
|
|
6033
|
+
// provider, which eth_sendTransaction-s it (the wallet signs + submits in one
|
|
6034
|
+
// step). Mirrors handlePreBuiltTransaction's shape but routes through the EVM
|
|
6035
|
+
// slot of runTransaction (Solana slot undefined). shouldSubmitTx is honored by
|
|
6036
|
+
// the provider itself.
|
|
6037
|
+
async function handleEvmTransaction(evmTransaction, authProvider, options) {
|
|
6038
|
+
return authProvider.runTransaction(evmTransaction, undefined, options);
|
|
6039
|
+
}
|
|
5366
6040
|
async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
|
|
5367
6041
|
var _a, _b, _c;
|
|
5368
6042
|
const config = await getConfig();
|
|
@@ -6743,7 +7417,7 @@ function sendUnsubscribe(connection, subscriptionId) {
|
|
|
6743
7417
|
* the subscription onto, since a WS connection routes to a single DO.
|
|
6744
7418
|
*/
|
|
6745
7419
|
async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
6746
|
-
var _a, _b;
|
|
7420
|
+
var _a, _b, _c, _d, _e;
|
|
6747
7421
|
const config = await getConfig();
|
|
6748
7422
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6749
7423
|
// Use explicit appId override if provided, otherwise fall back to config
|
|
@@ -6794,19 +7468,23 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6794
7468
|
const cachedEntry = cacheKey ? getResponseCacheEntry(cacheKey, identityToken) : undefined;
|
|
6795
7469
|
// Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
|
|
6796
7470
|
let existingSubscription;
|
|
7471
|
+
let specKey = '';
|
|
6797
7472
|
try {
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
7473
|
+
// Stringify THIS call's specs once; existing subscriptions carry their
|
|
7474
|
+
// precomputed specKey, so the scan is a string compare per entry
|
|
7475
|
+
// (previously each new subscribe re-stringified every existing
|
|
7476
|
+
// subscription's shape/filter/sort — O(N²) with many live hooks).
|
|
7477
|
+
specKey = JSON.stringify([
|
|
7478
|
+
(_a = subscriptionOptions.shape) !== null && _a !== void 0 ? _a : null,
|
|
7479
|
+
(_b = subscriptionOptions.filter) !== null && _b !== void 0 ? _b : null,
|
|
7480
|
+
(_c = subscriptionOptions.sort) !== null && _c !== void 0 ? _c : null,
|
|
7481
|
+
]);
|
|
6801
7482
|
for (const sub of connection.subscriptions.values()) {
|
|
6802
|
-
const subShapeKey = sub.shape ? JSON.stringify(sub.shape) : '';
|
|
6803
|
-
const subFilterKey = sub.filter ? JSON.stringify(sub.filter) : '';
|
|
6804
|
-
const subSortKey = sub.sort ? JSON.stringify(sub.sort) : '';
|
|
6805
7483
|
// H1: also require the same principal identity (compact hash AND exact
|
|
6806
7484
|
// token) — a different logged-in user must not piggy-back on (and
|
|
6807
7485
|
// inherit lastData from) another user's existing subscription, even
|
|
6808
7486
|
// with identical path/prompt/shape/filter/sort.
|
|
6809
|
-
if (sub.identity === identity && sub.identityToken === identityToken && sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt &&
|
|
7487
|
+
if (sub.identity === identity && sub.identityToken === identityToken && sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt && sub.specKey === specKey && sub.includeSubPaths === ((_d = subscriptionOptions.includeSubPaths) !== null && _d !== void 0 ? _d : false) && sub.limit === subscriptionOptions.limit && sub.cursor === subscriptionOptions.cursor) {
|
|
6810
7488
|
existingSubscription = sub;
|
|
6811
7489
|
break;
|
|
6812
7490
|
}
|
|
@@ -6899,7 +7577,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6899
7577
|
shape: subscriptionOptions.shape,
|
|
6900
7578
|
limit: subscriptionOptions.limit,
|
|
6901
7579
|
cursor: subscriptionOptions.cursor,
|
|
6902
|
-
includeSubPaths: (
|
|
7580
|
+
includeSubPaths: (_e = subscriptionOptions.includeSubPaths) !== null && _e !== void 0 ? _e : false,
|
|
6903
7581
|
callbacks: [subscriptionOptions],
|
|
6904
7582
|
lastData: undefined,
|
|
6905
7583
|
dataRevision: 0,
|
|
@@ -6907,6 +7585,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6907
7585
|
cache: responseCacheEnabled,
|
|
6908
7586
|
identity,
|
|
6909
7587
|
identityToken,
|
|
7588
|
+
specKey,
|
|
6910
7589
|
};
|
|
6911
7590
|
connection.subscriptions.set(subscriptionId, subscription);
|
|
6912
7591
|
}
|
|
@@ -7907,6 +8586,8 @@ function defineLiveModule(mod) {
|
|
|
7907
8586
|
return mod;
|
|
7908
8587
|
}
|
|
7909
8588
|
|
|
8589
|
+
exports.BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR;
|
|
8590
|
+
exports.BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR;
|
|
7910
8591
|
exports.BoundedDeclineError = BoundedDeclineError;
|
|
7911
8592
|
exports.DECLINE_EVENT_NAME = DECLINE_EVENT_NAME;
|
|
7912
8593
|
exports.EFFECT_INTENT_ADDRESS = EFFECT_INTENT_ADDRESS;
|
|
@@ -7917,6 +8598,8 @@ exports.ReactNativeSessionManager = ReactNativeSessionManager;
|
|
|
7917
8598
|
exports.ServerSessionManager = ServerSessionManager;
|
|
7918
8599
|
exports.WebSessionManager = WebSessionManager;
|
|
7919
8600
|
exports.aggregate = aggregate;
|
|
8601
|
+
exports.assertCanonicalBoundedRuntimeWriteCalldata = assertCanonicalBoundedRuntimeWriteCalldata;
|
|
8602
|
+
exports.assertEvmTransactionMatchesWrite = assertEvmTransactionMatchesWrite;
|
|
7920
8603
|
exports.buildSetDocumentsTransaction = buildSetDocumentsTransaction;
|
|
7921
8604
|
exports.clearCache = clearCache;
|
|
7922
8605
|
exports.closeAllSubscriptions = closeAllSubscriptions;
|
|
@@ -7930,6 +8613,7 @@ exports.dispatchDeclineEvent = dispatchDeclineEvent;
|
|
|
7930
8613
|
exports.docId = docId;
|
|
7931
8614
|
exports.functions = functions;
|
|
7932
8615
|
exports.genAuthNonce = genAuthNonce;
|
|
8616
|
+
exports.genSiweMessage = genSiweMessage;
|
|
7933
8617
|
exports.genSolanaMessage = genSolanaMessage;
|
|
7934
8618
|
exports.get = get;
|
|
7935
8619
|
exports.getActiveSessionManager = getActiveSessionManager;
|
|
@@ -7947,6 +8631,7 @@ exports.isEffectResult = isEffectResult;
|
|
|
7947
8631
|
exports.live = live;
|
|
7948
8632
|
exports.liveIntent = intent;
|
|
7949
8633
|
exports.liveStatus = status;
|
|
8634
|
+
exports.normalizeEvmAddress = normalizeEvmAddress;
|
|
7950
8635
|
exports.now = now;
|
|
7951
8636
|
exports.queryAggregate = queryAggregate;
|
|
7952
8637
|
exports.reconnectWithNewAuth = reconnectWithNewAuth;
|
|
@@ -7967,6 +8652,7 @@ exports.signSessionCreateMessage = signSessionCreateMessage;
|
|
|
7967
8652
|
exports.signTransaction = signTransaction;
|
|
7968
8653
|
exports.subscribe = subscribe;
|
|
7969
8654
|
exports.subscribeLiveView = subscribeView;
|
|
8655
|
+
exports.toLowerEvmAddress = toLowerEvmAddress;
|
|
7970
8656
|
exports.toMillis = toMillis;
|
|
7971
8657
|
exports.toSeconds = toSeconds;
|
|
7972
8658
|
exports.withEffects = withEffects;
|