From b772b2df9bfdf68f06c15429ab763e2c73108e21 Mon Sep 17 00:00:00 2001 From: Josh Long Date: Mon, 3 Jun 2024 16:27:36 -0400 Subject: [PATCH 1/2] Revert "Merge pull request #482 from LIT-Protocol/feature/lit-3139-identify-private-methods-in-litnodeclient" This reverts commit b8acf3db5a163612ab54bd4468de7b36560e971a, reversing changes made to 72fc82a98a46f28697c78cb6370f040e7cee0fb3. --- cypress/e2e/spec.cy.ts | 2 +- packages/core/src/lib/lit-core.ts | 45 +- .../src/lib/helpers/get-expiration.test.ts | 17 - .../src/lib/helpers/get-expiration.ts | 9 - .../src/lib/lit-node-client-nodejs.ts | 3127 +++++++++-------- .../src/lib/lit-node-client.ts | 4 +- packages/misc/src/lib/misc.spec.ts | 9 + packages/misc/src/lib/misc.ts | 23 + packages/types/src/lib/ILitNodeClient.ts | 106 + packages/types/src/lib/interfaces.ts | 13 + 10 files changed, 1835 insertions(+), 1520 deletions(-) delete mode 100644 packages/lit-node-client-nodejs/src/lib/helpers/get-expiration.test.ts delete mode 100644 packages/lit-node-client-nodejs/src/lib/helpers/get-expiration.ts diff --git a/cypress/e2e/spec.cy.ts b/cypress/e2e/spec.cy.ts index 093be6d346..2f7c61a44f 100644 --- a/cypress/e2e/spec.cy.ts +++ b/cypress/e2e/spec.cy.ts @@ -549,7 +549,7 @@ describe('Lit Action', () => { url: 'https://cayenne.litgateway.com:7371/web/execute', data, }; - const res = await savedParams.litNodeClient._sendCommandToNode(reqBody); + const res = await savedParams.litNodeClient.sendCommandToNode(reqBody); expect(res).to.have.property('success', true); }); diff --git a/packages/core/src/lib/lit-core.ts b/packages/core/src/lib/lit-core.ts index cdc5271d55..fca499a804 100644 --- a/packages/core/src/lib/lit-core.ts +++ b/packages/core/src/lib/lit-core.ts @@ -35,6 +35,7 @@ import { import { bootstrapLogManager, executeWithRetry, + getIpAddress, isBrowser, isNode, log, @@ -175,7 +176,7 @@ export class LitCore { } // -- set bootstrapUrls to match the network litNetwork unless it's set to custom - this.#setCustomBootstrapUrls(); + this.setCustomBootstrapUrls(); // -- set global variables globalThis.litConfig = this.config; @@ -439,9 +440,9 @@ export class LitCore { * that the client's configuration is always in sync with the current state of the * staking contract. * - * @returns { void } + * @returns {Promise} A promise that resolves when the listener is successfully set up. */ - #listenForNewEpoch(): void { + private _listenForNewEpoch() { // Check if we've already set up the listener to avoid duplicates if (this._stakingContractListener) { // Already listening, do nothing @@ -475,14 +476,13 @@ export class LitCore { if (globalThis.litConfig) delete globalThis.litConfig; } - protected _stopNetworkPolling() { + _stopNetworkPolling() { if (this._networkSyncInterval) { clearInterval(this._networkSyncInterval); this._networkSyncInterval = null; } } - - protected _stopListeningForNewEpoch() { + _stopListeningForNewEpoch() { if (this._stakingContract && this._stakingContractListener) { this._stakingContract.off('StateChanged', this._stakingContractListener); this._stakingContractListener = null; @@ -496,7 +496,7 @@ export class LitCore { * @returns { void } * */ - #setCustomBootstrapUrls = (): void => { + setCustomBootstrapUrls = (): void => { // -- validate if (this.config.litNetwork === 'custom') return; @@ -585,8 +585,8 @@ export class LitCore { await this._runHandshakeWithBootstrapUrls(); Object.assign(this, { ...coreNodeConfig, connectedNodes, serverKeys }); - this.#scheduleNetworkSync(); - this.#listenForNewEpoch(); + this._scheduleNetworkSync(); + this._listenForNewEpoch(); // FIXME: don't create global singleton; multiple instances of `core` should not all write to global // @ts-expect-error typeof globalThis is not defined. We're going to get rid of the global soon. @@ -617,7 +617,7 @@ export class LitCore { }): Promise { const challenge = this.getRandomHexString(64); - const handshakeResult = await this.#handshakeWithNode( + const handshakeResult = await this.handshakeWithNode( { url, challenge }, requestId ); @@ -707,7 +707,7 @@ export class LitCore { coreNodeConfig: CoreNodeConfig; }> { // -- handshake with each node - const requestId: string = this.#getRequestId(); + const requestId: string = this.getRequestId(); // track connectedNodes for the new handshake operation const connectedNodes = new Set(); @@ -858,7 +858,7 @@ export class LitCore { * We can remove this network sync code entirely if we refactor our code to fetch latest blockhash on-demand. * @private */ - #scheduleNetworkSync() { + private _scheduleNetworkSync() { if (this._networkSyncInterval) { clearInterval(this._networkSyncInterval); } @@ -880,7 +880,7 @@ export class LitCore { * @returns { string } * */ - #getRequestId(): string { + getRequestId() { return Math.random().toString(16).slice(2); } @@ -891,7 +891,7 @@ export class LitCore { * @returns { string } */ - getRandomHexString(size: number): string { + getRandomHexString(size: number) { return [...Array(size)] .map(() => Math.floor(Math.random() * 16).toString(16)) .join(''); @@ -905,7 +905,7 @@ export class LitCore { * @returns { Promise } * */ - #handshakeWithNode = async ( + handshakeWithNode = async ( params: HandshakeWithNode, requestId: string ): Promise => { @@ -927,7 +927,7 @@ export class LitCore { challenge: params.challenge, }; - return await this._sendCommandToNode({ + return await this.sendCommandToNode({ url: urlWithPath, data, requestId, @@ -985,7 +985,7 @@ export class LitCore { * @returns { Promise } * */ - protected _sendCommandToNode = async ({ + sendCommandToNode = async ({ url, data, requestId, @@ -1023,7 +1023,7 @@ export class LitCore { * @returns { Array> } * */ - protected _getNodePromises = ( + getNodePromises = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (url: string) => Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1048,7 +1048,7 @@ export class LitCore { * @returns The session signature for the given URL. * @throws An error if sessionSigs is not provided or if the session signature for the URL is not found. */ - protected _getSessionSigByUrl = ({ + getSessionSigByUrl = ({ sessionSigs, url, }: { @@ -1154,7 +1154,7 @@ export class LitCore { * @param { number } minNodeCount number of nodes we need valid results from in order to resolve * @returns { Promise | RejectedNodePromises> } */ - protected _handleNodePromises = async ( + handleNodePromises = async ( nodePromises: Promise[], requestId: string, minNodeCount: number @@ -1237,10 +1237,7 @@ export class LitCore { * @returns { void } * */ - protected _throwNodeError = ( - res: RejectedNodePromises, - requestId: string - ): void => { + _throwNodeError = (res: RejectedNodePromises, requestId: string): void => { if (res.error) { if ( ((res.error.errorCode && diff --git a/packages/lit-node-client-nodejs/src/lib/helpers/get-expiration.test.ts b/packages/lit-node-client-nodejs/src/lib/helpers/get-expiration.test.ts deleted file mode 100644 index a345e1586e..0000000000 --- a/packages/lit-node-client-nodejs/src/lib/helpers/get-expiration.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getExpiration } from './get-expiration'; - -describe('getExpiration', () => { - it('should return the expiration date and time as an ISO string', () => { - // Arrange - const currentDate = new Date(); - const expectedExpiration = new Date( - currentDate.getTime() + 1000 * 60 * 60 * 24 - ).toISOString(); - - // Act - const result = getExpiration(); - - // Assert - expect(result).toBe(expectedExpiration); - }); -}); diff --git a/packages/lit-node-client-nodejs/src/lib/helpers/get-expiration.ts b/packages/lit-node-client-nodejs/src/lib/helpers/get-expiration.ts deleted file mode 100644 index 73eef94d9d..0000000000 --- a/packages/lit-node-client-nodejs/src/lib/helpers/get-expiration.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Returns the expiration date and time as an ISO string. - * The expiration is set to 24 hours from the current date and time. - * - * @returns {string} The expiration date and time as an ISO string. - */ -export const getExpiration = () => { - return new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(); -}; diff --git a/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts b/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts index ca5df0f27a..59cdb0dca7 100644 --- a/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts +++ b/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts @@ -1,6 +1,6 @@ import { computeAddress } from '@ethersproject/transactions'; import { BigNumber, ethers } from 'ethers'; -import { sha256 } from 'ethers/lib/utils'; +import { joinSignature, sha256 } from 'ethers/lib/utils'; import { SiweMessage } from 'siwe'; import { @@ -28,6 +28,7 @@ import { } from '@lit-protocol/constants'; import { LitCore, composeLitUrl } from '@lit-protocol/core'; import { + combineEcdsaShares, combineSignatureShares, encrypt, generateSessionKeyPair, @@ -69,6 +70,7 @@ import type { CustomNetwork, DecryptRequest, DecryptResponse, + EncryptRequest, EncryptResponse, ExecuteJsResponse, FormattedMultipleAccs, @@ -88,10 +90,10 @@ import type { SessionKeyPair, SessionSigningTemplate, SessionSigsMap, + SigShare, SignSessionKeyProp, SignSessionKeyResponse, Signature, - SigningAccessControlConditionRequest, SuccessNodePromises, ILitNodeClient, GetPkpSessionSigs, @@ -107,14 +109,16 @@ import type { SigResponse, EncryptSdkParams, GetLitActionSessionSigs, + GetSignSessionKeySharesProp, EncryptionSignRequest, + SigningAccessControlConditionRequest, JsonPKPClaimKeyRequest, } from '@lit-protocol/types'; import * as blsSdk from '@lit-protocol/bls-sdk'; import { normalizeJsParams } from './helpers/normalize-params'; import { encodeCode } from './helpers/encode-code'; -import { getSignatures } from './helpers/get-signatures'; +import { getFlattenShare, getSignatures } from './helpers/get-signatures'; import { removeDoubleQuotes } from './helpers/remove-double-quotes'; import { parseAsJsonOrString } from './helpers/parse-as-json-or-string'; import { getClaimsList } from './helpers/get-claims-list'; @@ -123,7 +127,6 @@ import { normalizeArray } from './helpers/normalize-array'; import { parsePkpSignResponse } from './helpers/parse-pkp-sign-response'; import { getBlsSignatures } from './helpers/get-bls-signatures'; import { processLitActionResponseStrategy } from './helpers/process-lit-action-response-strategy'; -import { getExpiration } from './helpers/get-expiration'; export class LitNodeClientNodeJs extends LitCore @@ -148,196 +151,186 @@ export class LitNodeClientNodeJs } } - // ========== Private Methods ========== - /** - * Handles the authentication callback and updates the storage item with the authentication signature. - * @param authCallbackParams - The parameters required for the authentication callback. - * @param authCallback - The optional authentication callback function. - * @returns A promise that resolves to the authentication signature. - * @throws An error if no default authentication callback is provided. - */ - #authCallbackAndUpdateStorageItem = async ({ - authCallbackParams, - authCallback, - }: { - authCallbackParams: AuthCallbackParams; - authCallback?: AuthCallback; - }): Promise => { - let authSig: AuthSig; + // ========== Rate Limit NFT ========== - if (authCallback) { - authSig = await authCallback(authCallbackParams); - } else { - if (!this.defaultAuthCallback) { - return throwError({ - message: 'No default auth callback provided', - errorKind: LIT_ERROR.PARAMS_MISSING_ERROR.kind, - errorCode: LIT_ERROR.PARAMS_MISSING_ERROR.name, - }); - } - authSig = await this.defaultAuthCallback(authCallbackParams); + // TODO: Add support for browser feature/lit-2321-js-sdk-add-browser-support-for-createCapacityDelegationAuthSig + createCapacityDelegationAuthSig = async ( + params: CapacityCreditsReq + ): Promise => { + // -- validate + if (!params.dAppOwnerWallet) { + throw new Error('dAppOwnerWallet must exist'); } - // (TRY) to set walletSig to local storage - const storeNewWalletSigOrError = setStorageItem( - LOCAL_STORAGE_KEYS.WALLET_SIGNATURE, - JSON.stringify(authSig) - ); - if (storeNewWalletSigOrError.type === EITHER_TYPE.SUCCESS) { - return authSig; + // Useful log for debugging + if (!params.delegateeAddresses || params.delegateeAddresses.length === 0) { + log( + `[createCapacityDelegationAuthSig] 'delegateeAddresses' is an empty array. It means that no body can use it. However, if the 'delegateeAddresses' field is omitted, It means that the capability will not restrict access based on delegatee list, but it may still enforce other restrictions such as usage limits (uses) and specific NFT IDs (nft_id).` + ); } - // Setting local storage failed, try to remove the item key. - console.warn( - `Unable to store walletSig in local storage. Not a problem. Continuing to remove item key...` - ); - const removeWalletSigOrError = removeStorageItem( - LOCAL_STORAGE_KEYS.WALLET_SIGNATURE + // -- This is the owner address who holds the Capacity Credits NFT token and wants to delegate its + // usage to a list of delegatee addresses + const dAppOwnerWalletAddress = ethers.utils.getAddress( + await params.dAppOwnerWallet.getAddress() ); - if (removeWalletSigOrError.type === EITHER_TYPE.ERROR) { - console.warn( - `Unable to remove walletSig in local storage. Not a problem. Continuing...` - ); + + // -- if it's not ready yet, then connect + if (!this.ready) { + await this.connect(); } - return authSig; + const nonce = await this.getLatestBlockhash(); + const siweMessage = await createSiweMessageWithCapacityDelegation({ + uri: 'lit:capability:delegation', + litNodeClient: this, + walletAddress: dAppOwnerWalletAddress, + nonce: nonce, + expiration: params.expiration, + domain: params.domain, + statement: params.statement, + + // -- capacity delegation specific configuration + uses: params.uses, + delegateeAddresses: params.delegateeAddresses, + capacityTokenId: params.capacityTokenId, + }); + + const authSig = await generateAuthSig({ + signer: params.dAppOwnerWallet, + toSign: siweMessage, + }); + + return { capacityDelegationAuthSig: authSig }; }; + + // ========== Scoped Class Helpers ========== + /** * - * Check if a session key needs to be resigned. These are the scenarios where a session key needs to be resigned: - * 1. The authSig.sig does not verify successfully against the authSig.signedMessage - * 2. The authSig.signedMessage.uri does not match the sessionKeyUri - * 3. The authSig.signedMessage does not contain at least one session capability object + * we need to send jwt params iat (issued at) and exp (expiration) because the nodes may have different wall clock times, the nodes will verify that these params are withing a grace period * */ - #checkNeedToResignSessionKey = async ({ - authSig, - sessionKeyUri, - resourceAbilityRequests, - }: { - authSig: AuthSig; - sessionKeyUri: any; - resourceAbilityRequests: LitResourceAbilityRequest[]; - }): Promise => { - const authSigSiweMessage = new SiweMessage(authSig.signedMessage); + getJWTParams = () => { + const now = Date.now(); + const iat = Math.floor(now / 1000); + const exp = iat + 12 * 60 * 60; // 12 hours in seconds - try { - await authSigSiweMessage.validate(authSig.sig); - } catch (e) { - console.debug('Need retry because verify failed', e); - return true; - } + return { iat, exp }; + }; - // make sure the sig is for the correct session key - if (authSigSiweMessage.uri !== sessionKeyUri) { - console.debug('Need retry because uri does not match'); - return true; - } + // ==================== SESSIONS ==================== + /** + * Try to get the session key in the local storage, + * if not, generates one. + * @return { SessionKeyPair } session key pair + */ + getSessionKey = (): SessionKeyPair => { + const storageKey = LOCAL_STORAGE_KEYS.SESSION_KEY; + const storedSessionKeyOrError = getStorageItem(storageKey); - // make sure the authSig contains at least one resource. if ( - !authSigSiweMessage.resources || - authSigSiweMessage.resources.length === 0 + storedSessionKeyOrError.type === EITHER_TYPE.ERROR || + !storedSessionKeyOrError.result || + storedSessionKeyOrError.result === '' ) { - console.debug('Need retry because empty resources'); - return true; - } + console.warn( + `Storage key "${storageKey}" is missing. Not a problem. Contiune...` + ); - // make sure the authSig contains session capabilities that can be parsed. - // TODO: we currently only support the first resource being a session capability object. - const authSigSessionCapabilityObject = decode( - authSigSiweMessage.resources[0] - ); + // Generate new one + const newSessionKey = generateSessionKeyPair(); - // make sure the authSig session capability object describes capabilities that are equal or greater than - // the abilities requested against the resources in the resource ability requests. - for (const resourceAbilityRequest of resourceAbilityRequests) { - if ( - !authSigSessionCapabilityObject.verifyCapabilitiesForResource( - resourceAbilityRequest.resource, - resourceAbilityRequest.ability - ) - ) { - console.debug('Need retry because capabilities do not match', { - authSigSessionCapabilityObject, - resourceAbilityRequest, - }); - return true; + // (TRY) to set to local storage + try { + localStorage.setItem(storageKey, JSON.stringify(newSessionKey)); + } catch (e) { + log( + `[getSessionKey] Localstorage not available.Not a problem.Contiune...` + ); } - } - return false; + return newSessionKey; + } else { + return JSON.parse(storedSessionKeyOrError.result as string); + } }; + /** - * Decrypts the ciphertext using the provided signature shares. + * Check if a given object is of type SessionKeyPair. * - * @param networkPubKey - The network public key. - * @param identityParam - The identity parameter. - * @param ciphertext - The ciphertext to decrypt. - * @param signatureShares - An array of signature shares. - * @returns The decrypted data as a Uint8Array. + * @param obj - The object to check. + * @returns True if the object is of type SessionKeyPair. */ - #decryptWithSignatureShares = ( - networkPubKey: string, - identityParam: Uint8Array, - ciphertext: string, - signatureShares: NodeBlsSigningShare[] - ): Uint8Array => { - const sigShares = signatureShares.map((s: any) => s.signatureShare); - - return verifyAndDecryptWithSignatureShares( - networkPubKey, - identityParam, - ciphertext, - sigShares + isSessionKeyPair(obj: any): obj is SessionKeyPair { + return ( + typeof obj === 'object' && + 'publicKey' in obj && + 'secretKey' in obj && + typeof obj.publicKey === 'string' && + typeof obj.secretKey === 'string' ); - }; - /** - * Checks if the given response is a success node promise. - * @private - * @param res - The response object to check. - * @returns A boolean indicating whether the response is a success node promise. - * @template T - The type of the success node promise. - */ - #isSuccessNodePromises = (res: any): res is SuccessNodePromises => { - return res.success === true; - }; + } + /** - * Generates an identity parameter for encryption based on the provided conditions and private data. - * @param hashOfConditionsStr - The hash of the conditions string. - * @param hashOfPrivateDataStr - The hash of the private data string. - * @returns The generated identity parameter for encryption. + * Generates wildcard capability for each of the LIT resources + * specified. + * @param litResources is an array of LIT resources + * @param addAllCapabilities is a boolean that specifies whether to add all capabilities for each resource */ - #getIdentityParamForEncryption = ( - hashOfConditionsStr: string, - hashOfPrivateDataStr: string - ): string => { - return new LitAccessControlConditionResource( - `${hashOfConditionsStr}/${hashOfPrivateDataStr}` - ).getResourceKey(); - }; + static async generateSessionCapabilityObjectWithWildcards( + litResources: ILitResource[], + addAllCapabilities?: boolean, + rateLimitAuthSig?: AuthSig + ): Promise { + const sessionCapabilityObject = new RecapSessionCapabilityObject({}, []); + + // disable for now + const _addAllCapabilities = addAllCapabilities ?? false; + + if (_addAllCapabilities) { + for (const litResource of litResources) { + sessionCapabilityObject.addAllCapabilitiesForResource(litResource); + } + } + + if (rateLimitAuthSig) { + throw new Error('Not implemented yet.'); + // await sessionCapabilityObject.addRateLimitAuthSig(rateLimitAuthSig); + } + + return sessionCapabilityObject; + } + + // backward compatibility + async generateSessionCapabilityObjectWithWildcards( + litResources: ILitResource[] + ): Promise { + return await LitNodeClientNodeJs.generateSessionCapabilityObjectWithWildcards( + litResources + ); + } + /** - * we need to send jwt params iat (issued at) and exp (expiration) because the nodes may have - * different wall clock times, the nodes will verify that these params are withing a grace period * - * @returns { { iat: number, exp: number } } the jwt params + * Get expiration for session default time is 1 day / 24 hours + * */ - #getJWTParams = (): { - iat: number; - exp: number; - } => { - const now = Date.now(); - const iat = Math.floor(now / 1000); - const exp = iat + 12 * 60 * 60; // 12 hours in seconds + static getExpiration = () => { + return new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(); + }; - return { iat, exp }; + // backward compatibility + getExpiration = () => { + return LitNodeClientNodeJs.getExpiration(); }; + /** * * Get the signature from local storage, if not, generates one * */ - #getWalletSig = async ({ + getWalletSig = async ({ authNeededCallback, chain, sessionCapabilityObject, @@ -454,16 +447,133 @@ export class LitNodeClientNodeJs log('getWalletSig - flow 3'); return walletSig!; }; - /** - * - * Combine Shares from network public key set and signature shares + + #authCallbackAndUpdateStorageItem = async ({ + authCallbackParams, + authCallback, + }: { + authCallbackParams: AuthCallbackParams; + authCallback?: AuthCallback; + }): Promise => { + let authSig: AuthSig; + + if (authCallback) { + authSig = await authCallback(authCallbackParams); + } else { + if (!this.defaultAuthCallback) { + return throwError({ + message: 'No default auth callback provided', + errorKind: LIT_ERROR.PARAMS_MISSING_ERROR.kind, + errorCode: LIT_ERROR.PARAMS_MISSING_ERROR.name, + }); + } + authSig = await this.defaultAuthCallback(authCallbackParams); + } + + // (TRY) to set walletSig to local storage + const storeNewWalletSigOrError = setStorageItem( + LOCAL_STORAGE_KEYS.WALLET_SIGNATURE, + JSON.stringify(authSig) + ); + if (storeNewWalletSigOrError.type === EITHER_TYPE.SUCCESS) { + return authSig; + } + + // Setting local storage failed, try to remove the item key. + console.warn( + `Unable to store walletSig in local storage. Not a problem. Continuing to remove item key...` + ); + const removeWalletSigOrError = removeStorageItem( + LOCAL_STORAGE_KEYS.WALLET_SIGNATURE + ); + if (removeWalletSigOrError.type === EITHER_TYPE.ERROR) { + console.warn( + `Unable to remove walletSig in local storage. Not a problem. Continuing...` + ); + } + + return authSig; + }; + + /** + * + * Check if a session key needs to be resigned. These are the scenarios where a session key needs to be resigned: + * 1. The authSig.sig does not verify successfully against the authSig.signedMessage + * 2. The authSig.signedMessage.uri does not match the sessionKeyUri + * 3. The authSig.signedMessage does not contain at least one session capability object + * + */ + checkNeedToResignSessionKey = async ({ + authSig, + sessionKeyUri, + resourceAbilityRequests, + }: { + authSig: AuthSig; + sessionKeyUri: any; + resourceAbilityRequests: LitResourceAbilityRequest[]; + }): Promise => { + const authSigSiweMessage = new SiweMessage(authSig.signedMessage); + + try { + await authSigSiweMessage.validate(authSig.sig); + } catch (e) { + console.debug('Need retry because verify failed', e); + return true; + } + + // make sure the sig is for the correct session key + if (authSigSiweMessage.uri !== sessionKeyUri) { + console.debug('Need retry because uri does not match'); + return true; + } + + // make sure the authSig contains at least one resource. + if ( + !authSigSiweMessage.resources || + authSigSiweMessage.resources.length === 0 + ) { + console.debug('Need retry because empty resources'); + return true; + } + + // make sure the authSig contains session capabilities that can be parsed. + // TODO: we currently only support the first resource being a session capability object. + const authSigSessionCapabilityObject = decode( + authSigSiweMessage.resources[0] + ); + + // make sure the authSig session capability object describes capabilities that are equal or greater than + // the abilities requested against the resources in the resource ability requests. + for (const resourceAbilityRequest of resourceAbilityRequests) { + if ( + !authSigSessionCapabilityObject.verifyCapabilitiesForResource( + resourceAbilityRequest.resource, + resourceAbilityRequest.ability + ) + ) { + console.debug('Need retry because capabilities do not match', { + authSigSessionCapabilityObject, + resourceAbilityRequest, + }); + return true; + } + } + + return false; + }; + + // ==================== API Calls to Nodes ==================== + + /** + * + * Combine Shares from network public key set and signature shares * * @param { NodeBlsSigningShare } signatureShares * * @returns { string } final JWT (convert the sig to base64 and append to the jwt) * */ - #combineSharesAndGetJWT = ( + combineSharesAndGetJWT = ( signatureShares: NodeBlsSigningShare[], requestId: string = '' ): string => { @@ -503,247 +613,77 @@ export class LitNodeClientNodeJs return finalJwt; }; - /** - * - * Get Session Key URI eg. lit:session:0x1234 - * - * @param publicKey is the public key of the session key - * @returns { string } the session key uri - */ - #getSessionKeyUri = (publicKey: string): string => { - return LIT_SESSION_KEY_URI + publicKey; - }; - /** - * Generates a promise by sending a command to the Lit node - * - * @param url - The URL to send the command to. - * @param params - The parameters to include in the command. - * @param requestId - The ID of the request. - * @returns A promise that resolves with the response from the server. - */ - #generatePromise = async ( - url: string, - params: any, - requestId: string - ): Promise => { - return await this._sendCommandToNode({ - url, - data: params, - requestId, - }); - }; - // ========== Rate Limit NFT ========== - - // TODO: Add support for browser feature/lit-2321-js-sdk-add-browser-support-for-createCapacityDelegationAuthSig - /** - * Creates a capacity delegation authSig. - * - * @param params - The parameters for creating the capacity delegation authSig. - * @returns A promise that resolves to the capacity delegation authSig. - * @throws An error if the dAppOwnerWallet is not provided. - */ - createCapacityDelegationAuthSig = async ( - params: CapacityCreditsReq - ): Promise => { - // -- validate - if (!params.dAppOwnerWallet) { - throw new Error('dAppOwnerWallet must exist'); - } - // Useful log for debugging - if (!params.delegateeAddresses || params.delegateeAddresses.length === 0) { - log( - `[createCapacityDelegationAuthSig] 'delegateeAddresses' is an empty array. It means that no body can use it. However, if the 'delegateeAddresses' field is omitted, It means that the capability will not restrict access based on delegatee list, but it may still enforce other restrictions such as usage limits (uses) and specific NFT IDs (nft_id).` - ); - } + #decryptWithSignatureShares = ( + networkPubKey: string, + identityParam: Uint8Array, + ciphertext: string, + signatureShares: NodeBlsSigningShare[] + ): Uint8Array => { + const sigShares = signatureShares.map((s: any) => s.signatureShare); - // -- This is the owner address who holds the Capacity Credits NFT token and wants to delegate its - // usage to a list of delegatee addresses - const dAppOwnerWalletAddress = ethers.utils.getAddress( - await params.dAppOwnerWallet.getAddress() + return verifyAndDecryptWithSignatureShares( + networkPubKey, + identityParam, + ciphertext, + sigShares ); + }; - // -- if it's not ready yet, then connect - if (!this.ready) { - await this.connect(); - } + // ========== Promise Handlers ========== + getIpfsId = async ({ + dataToHash, + sessionSigs, + }: { + dataToHash: string; + sessionSigs: SessionSigsMap; + debug?: boolean; + }) => { + const res = await this.executeJs({ + ipfsId: LIT_ACTION_IPFS_HASH, + sessionSigs, + jsParams: { + dataToHash, + }, + }).catch((e) => { + logError('Error getting IPFS ID', e); + throw e; + }); - const nonce = await this.getLatestBlockhash(); - const siweMessage = await createSiweMessageWithCapacityDelegation({ - uri: 'lit:capability:delegation', - litNodeClient: this, - walletAddress: dAppOwnerWalletAddress, - nonce: nonce, - expiration: params.expiration, - domain: params.domain, - statement: params.statement, + let data; - // -- capacity delegation specific configuration - uses: params.uses, - delegateeAddresses: params.delegateeAddresses, - capacityTokenId: params.capacityTokenId, - }); + if (typeof res.response === 'string') { + try { + data = JSON.parse(res.response).res; + } catch (e) { + data = res.response; + } + } - const authSig = await generateAuthSig({ - signer: params.dAppOwnerWallet, - toSign: siweMessage, - }); + if (!data.success) { + logError('Error getting IPFS ID', data.data); + } - return { capacityDelegationAuthSig: authSig }; + return data.data; }; - // ==================== SESSIONS ==================== /** - * Try to get the session key in the local storage, - * if not, generates one. - * @return { SessionKeyPair } session key pair + * Run lit action on a single deterministicly selected node. It's important that the nodes use the same deterministic selection algorithm. + * + * Lit Action: dataToHash -> IPFS CID + * QmUjX8MW6StQ7NKNdaS6g4RMkvN5hcgtKmEi8Mca6oX4t3 + * + * @param { ExecuteJsProps } params + * + * @returns { Promise | RejectedNodePromises> } + * */ - getSessionKey = (): SessionKeyPair => { - const storageKey = LOCAL_STORAGE_KEYS.SESSION_KEY; - const storedSessionKeyOrError = getStorageItem(storageKey); - - if ( - storedSessionKeyOrError.type === EITHER_TYPE.ERROR || - !storedSessionKeyOrError.result || - storedSessionKeyOrError.result === '' - ) { - console.warn( - `Storage key "${storageKey}" is missing. Not a problem. Contiune...` - ); - - // Generate new one - const newSessionKey = generateSessionKeyPair(); - - // (TRY) to set to local storage - try { - localStorage.setItem(storageKey, JSON.stringify(newSessionKey)); - } catch (e) { - log( - `[getSessionKey] Localstorage not available.Not a problem.Contiune...` - ); - } - - return newSessionKey; - } else { - return JSON.parse(storedSessionKeyOrError.result as string); - } - }; - - /** - * Check if a given object is of type SessionKeyPair. - * - * @param obj - The object to check. - * @returns True if the object is of type SessionKeyPair. - */ - isSessionKeyPair(obj: any): obj is SessionKeyPair { - return ( - typeof obj === 'object' && - 'publicKey' in obj && - 'secretKey' in obj && - typeof obj.publicKey === 'string' && - typeof obj.secretKey === 'string' - ); - } - - /** - * Generates wildcard capability for each of the LIT resources - * specified. - * @param litResources is an array of LIT resources - * @param addAllCapabilities is a boolean that specifies whether to add all capabilities for each resource - */ - static async generateSessionCapabilityObjectWithWildcards( - litResources: ILitResource[], - addAllCapabilities?: boolean - ): Promise { - const sessionCapabilityObject = new RecapSessionCapabilityObject({}, []); - - // disable for now - const _addAllCapabilities = addAllCapabilities ?? false; - - if (_addAllCapabilities) { - for (const litResource of litResources) { - sessionCapabilityObject.addAllCapabilitiesForResource(litResource); - } - } - - return sessionCapabilityObject; - } - - // backward compatibility - async generateSessionCapabilityObjectWithWildcards( - litResources: ILitResource[] - ): Promise { - return await LitNodeClientNodeJs.generateSessionCapabilityObjectWithWildcards( - litResources - ); - } - - /** - * Get expiration for session default time is 1 day / 24 hours - */ - static getExpiration = () => { - return getExpiration(); - }; - - // backward compatibility - getExpiration = () => { - return getExpiration(); - }; - - // ========== Promise Handlers ========== - getIpfsId = async ({ - dataToHash, - sessionSigs, - }: { - dataToHash: string; - sessionSigs: SessionSigsMap; - debug?: boolean; - }) => { - const res = await this.executeJs({ - ipfsId: LIT_ACTION_IPFS_HASH, - sessionSigs, - jsParams: { - dataToHash, - }, - }).catch((e) => { - logError('Error getting IPFS ID', e); - throw e; - }); - - let data; - - if (typeof res.response === 'string') { - try { - data = JSON.parse(res.response).res; - } catch (e) { - data = res.response; - } - } - - if (!data.success) { - logError('Error getting IPFS ID', data.data); - } - - return data.data; - }; - - /** - * Run lit action on a single deterministicly selected node. It's important that the nodes use the same deterministic selection algorithm. - * - * Lit Action: dataToHash -> IPFS CID - * QmUjX8MW6StQ7NKNdaS6g4RMkvN5hcgtKmEi8Mca6oX4t3 - * - * @param { ExecuteJsProps } params - * - * @returns { Promise | RejectedNodePromises> } - * - */ - runOnTargetedNodes = async ( - params: JsonExecutionSdkParamsTargetNode - ): Promise< - SuccessNodePromises | RejectedNodePromises - > => { - log('running runOnTargetedNodes:', params.targetNodeRange); + runOnTargetedNodes = async ( + params: JsonExecutionSdkParamsTargetNode + ): Promise< + SuccessNodePromises | RejectedNodePromises + > => { + log('running runOnTargetedNodes:', params.targetNodeRange); if (!params.targetNodeRange) { return throwError({ @@ -808,7 +748,7 @@ export class LitNodeClientNodeJs log(`running on node ${nodeIndex} at ${url}`); // -- choose the right signature - const sessionSig = this._getSessionSigByUrl({ + const sessionSig = this.getSessionSigByUrl({ sessionSigs: params.sessionSigs, url, }); @@ -821,7 +761,7 @@ export class LitNodeClientNodeJs // this return { url: string, data: JsonRequest } // const singleNodePromise = this.getJsExecutionShares(url, reqBody, id); - const singleNodePromise = this._sendCommandToNode({ + const singleNodePromise = this.sendCommandToNode({ url: url, data: params, requestId: id, @@ -830,7 +770,7 @@ export class LitNodeClientNodeJs nodePromises.push(singleNodePromise); } - const handledPromise = (await this._handleNodePromises( + const handledPromise = (await this.handleNodePromises( nodePromises, id, params.targetNodeRange @@ -853,37 +793,184 @@ export class LitNodeClientNodeJs /** * - * Encrypt data using the LIT network public key. + * Get signatures from signed data * - * @param { EncryptSdkParams } params - * @param params.dataToEncrypt - The data to encrypt - * @param params.accessControlConditions - (optional) The access control conditions for the data - * @param params.evmContractConditions - (optional) The EVM contract conditions for the data - * @param params.solRpcConditions - (optional) The Solidity RPC conditions for the data - * @param params.unifiedAccessControlConditions - (optional) The unified access control conditions for the data + * @param { Array } signedData * - * @return { Promise } The encrypted ciphertext and the hash of the data + * @returns { any } * - * @throws { Error } if the LIT node client is not ready - * @throws { Error } if the subnetPubKey is null */ - encrypt = async (params: EncryptSdkParams): Promise => { + getSessionSignatures = (signedData: any[]): any => { + // -- prepare + const signatures: any = {}; + + // TOOD: get keys of signedData + const keys = Object.keys(signedData[0]); + + // removeExtraBackslashesAndQuotes + const sanitise = (str: string) => { + // Check if str is a string and remove extra backslashes + if (typeof str === 'string') { + // Remove backslashes + let newStr = str.replace(/\\+/g, ''); + // Remove leading and trailing double quotes + newStr = newStr.replace(/^"|"$/g, ''); + return newStr; + } + return str; + }; + + // -- execute + keys.forEach((key: any) => { + log('key:', key); + + const shares = signedData.map((r: any) => r[key]); + + log('shares:', shares); + + shares.sort((a: any, b: any) => a.shareIndex - b.shareIndex); + + const sigShares: SigShare[] = shares.map((s: any, index: number) => { + log('Original Share Struct:', s); + + const share = getFlattenShare(s); + + log('share:', share); + + if (!share) { + throw new Error('share is null or undefined'); + } + + if (!share.bigr) { + throw new Error( + `bigR is missing in share ${index}. share ${JSON.stringify(share)}` + ); + } + + const sanitisedBigR = sanitise(share.bigr); + const sanitisedSigShare = sanitise(share.publicKey); + + log('sanitisedBigR:', sanitisedBigR); + log('sanitisedSigShare:', sanitisedSigShare); + + return { + sigType: share.sigType, + signatureShare: sanitise(share.signatureShare), + shareIndex: share.shareIndex, + bigR: sanitise(share.bigr), + publicKey: share.publicKey, + dataSigned: share.dataSigned, + siweMessage: share.siweMessage, + }; + }); + + log('getSessionSignatures - sigShares', sigShares); + + const sigType = mostCommonString(sigShares.map((s: any) => s.sigType)); + + // -- validate if this.networkPubKeySet is null + if (this.networkPubKeySet === null) { + throwError({ + message: 'networkPubKeySet cannot be null', + errorKind: LIT_ERROR.PARAM_NULL_ERROR.kind, + errorCode: LIT_ERROR.PARAM_NULL_ERROR.name, + }); + return; + } + + // -- validate if signature type is ECDSA + if ( + sigType !== LIT_CURVE.EcdsaCaitSith && + sigType !== LIT_CURVE.EcdsaK256 && + sigType !== LIT_CURVE.EcdsaCAITSITHP256 + ) { + throwError({ + message: `signature type is ${sigType} which is invalid`, + errorKind: LIT_ERROR.UNKNOWN_SIGNATURE_TYPE.kind, + errorCode: LIT_ERROR.UNKNOWN_SIGNATURE_TYPE.name, + }); + return; + } + + const signature: any = combineEcdsaShares(sigShares); + if (!signature.r) { + throwError({ + message: 'siganture could not be combined', + errorKind: LIT_ERROR.UNKNOWN_SIGNATURE_ERROR.kind, + errorCode: LIT_ERROR.UNKNOWN_SIGNATURE_ERROR.name, + }); + } + + const encodedSig = joinSignature({ + r: '0x' + signature.r, + s: '0x' + signature.s, + v: signature.recid, + }); + + signatures[key] = { + ...signature, + signature: encodedSig, + publicKey: mostCommonString(sigShares.map((s: any) => s.publicKey)), + dataSigned: mostCommonString(sigShares.map((s: any) => s.dataSigned)), + siweMessage: mostCommonString(sigShares.map((s) => s.siweMessage)), + }; + }); + + return signatures; + }; + + /** + * + * Get a single signature + * + * @param { Array } shareData from all node promises + * + * @returns { string } signature + * + */ + getSignature = async (shareData: any[], requestId: string): Promise => { + // R_x & R_y values can come from any node (they will be different per node), and will generate a valid signature + const R_x = shareData[0].local_x; + const R_y = shareData[0].local_y; + + const valid_shares = shareData.map((s: any) => s.signature_share); + const shares = JSON.stringify(valid_shares); + + await wasmECDSA.initWasmEcdsaSdk(); // init WASM + const signature = wasmECDSA.combine_signature(R_x, R_y, shares); + logWithRequestId(requestId, 'raw ecdsa sig', signature); + + return signature; + }; + + // ========== Scoped Business Logics ========== + + // Normalize the data to a basic array + + // TODO: executeJsWithTargettedNodes + // if (formattedParams.targetNodeRange) { + // // FIXME: we should make this a separate function + // res = await this.runOnTargetedNodes(formattedParams); + // } + + /** + * + * Execute JS on the nodes and combine and return any resulting signatures + * + * @param { JsonExecutionSdkParams } params + * + * @returns { ExecuteJsResponse } + * + */ + executeJs = async ( + params: JsonExecutionSdkParams + ): Promise => { // ========== Validate Params ========== - // -- validate if it's ready if (!this.ready) { const message = - '6 LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; - throwError({ - message, - errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, - errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, - }); - } + '[executeJs] LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; - // -- validate if this.subnetPubKey is null - if (!this.subnetPubKey) { - const message = 'subnetPubKey cannot be null'; - return throwError({ + throwError({ message, errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, @@ -891,360 +978,326 @@ export class LitNodeClientNodeJs } const paramsIsSafe = safeParams({ - functionName: 'encrypt', - params, + functionName: 'executeJs', + params: params, }); if (!paramsIsSafe) { return throwError({ - message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, - errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, - errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + message: 'executeJs params are not valid', + errorKind: LIT_ERROR.INVALID_PARAM_TYPE.kind, + errorCode: LIT_ERROR.INVALID_PARAM_TYPE.name, }); } - // ========== Validate Access Control Conditions Schema ========== - await this.validateAccessControlConditionsSchema(params); - - // ========== Hashing Access Control Conditions ========= - // hash the access control conditions - const hashOfConditions: ArrayBuffer | undefined = - await this.getHashedAccessControlConditions(params); + // Format the params + const formattedParams: JsonExecutionSdkParams = { + ...params, + ...(params.jsParams && { jsParams: normalizeJsParams(params.jsParams) }), + ...(params.code && { code: encodeCode(params.code) }), + }; - if (!hashOfConditions) { - return throwError({ - message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, - errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, - errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + // ========== Get Node Promises ========== + // Handle promises for commands sent to Lit nodes + const wrapper = async ( + requestId: string + ): Promise | RejectedNodePromises> => { + const nodePromises = this.getNodePromises(async (url: string) => { + // -- choose the right signature + const sessionSig = this.getSessionSigByUrl({ + sessionSigs: formattedParams.sessionSigs, + url, + }); + + const reqBody: JsonExecutionRequest = { + ...formattedParams, + authSig: sessionSig, + }; + + const urlWithPath = composeLitUrl({ + url, + endpoint: LIT_ENDPOINT.EXECUTE_JS, + }); + + return this.generatePromise(urlWithPath, reqBody, requestId); }); - } - const hashOfConditionsStr = uint8arrayToString( - new Uint8Array(hashOfConditions), - 'base16' - ); + // -- resolve promises + const res = await this.handleNodePromises( + nodePromises, + requestId, + this.connectedNodes.size + ); - // ========== Hashing Private Data ========== - // hash the private data - const hashOfPrivateData = await crypto.subtle.digest( - 'SHA-256', - params.dataToEncrypt - ); - const hashOfPrivateDataStr = uint8arrayToString( - new Uint8Array(hashOfPrivateData), - 'base16' - ); + return res; + }; // wrapper end - // ========== Assemble identity parameter ========== - const identityParam = this.#getIdentityParamForEncryption( - hashOfConditionsStr, - hashOfPrivateDataStr + // ========== Execute with Retry ========== + const res = await executeWithRetry< + RejectedNodePromises | SuccessNodePromises + >( + wrapper, + (error: any, requestId: string, isFinal: boolean) => { + logError('an error occured, attempting to retry operation'); + }, + this.config.retryTolerance ); - // ========== Encrypt ========== - const ciphertext = encrypt( - this.subnetPubKey, - params.dataToEncrypt, - uint8arrayFromString(identityParam, 'utf8') - ); + // ========== Handle Response ========== + const requestId = res.requestId; - return { ciphertext, dataToEncryptHash: hashOfPrivateDataStr }; - }; + // -- case: promises rejected + if (!res.success) { + this._throwNodeError(res as RejectedNodePromises, requestId); + } - /** ============================== SESSION ============================== */ - /** - * Get session signatures for a set of resources - * - * High level, how this works: - * 1. Generate or retrieve session key - * 2. Generate or retrieve the wallet signature of the session key - * 3. Sign the specific resources with the session key - * - * Note: When generating session signatures for different PKPs or auth methods, - * be sure to call disconnectWeb3 to clear auth signatures stored in local storage - * - * @param { GetSessionSigsProps } params - * - * @example - * - * ```ts - * import { LitPKPResource, LitActionResource } from "@lit-protocol/auth-helpers"; -import { LitAbility } from "@lit-protocol/types"; -import { logWithRequestId } from '../../../misc/src/lib/misc'; + // -- case: promises success (TODO: check the keys of "values") + const responseData = (res as SuccessNodePromises).values; -const resourceAbilityRequests = [ - { - resource: new LitPKPResource("*"), - ability: LitAbility.PKPSigning, - }, - { - resource: new LitActionResource("*"), - ability: LitAbility.LitActionExecution, - }, - ]; - * ``` - */ - getSessionSigs = async ( - params: GetSessionSigsProps - ): Promise => { - // -- prepare - // Try to get it from local storage, if not generates one~ - const sessionKey = params.sessionKey ?? this.getSessionKey(); + logWithRequestId( + requestId, + 'executeJs responseData from node : ', + JSON.stringify(responseData, null, 2) + ); + + // -- find the responseData that has the most common response + const mostCommonResponse = findMostCommonResponse( + responseData + ) as NodeShare; - const sessionKeyUri = this.#getSessionKeyUri(sessionKey.publicKey); + const responseFromStrategy: any = processLitActionResponseStrategy( + responseData, + params.responseStrategy ?? { strategy: 'leastCommon' } + ); + mostCommonResponse.response = responseFromStrategy; - // First get or generate the session capability object for the specified resources. - const sessionCapabilityObject = params.sessionCapabilityObject - ? params.sessionCapabilityObject - : await this.generateSessionCapabilityObjectWithWildcards( - params.resourceAbilityRequests.map((r) => r.resource) - ); - const expiration = params.expiration || LitNodeClientNodeJs.getExpiration(); + const isSuccess = mostCommonResponse.success; + const hasSignedData = Object.keys(mostCommonResponse.signedData).length > 0; + const hasClaimData = Object.keys(mostCommonResponse.claimData).length > 0; - if (!this.latestBlockhash) { - throwError({ - message: 'Eth Blockhash is undefined.', - errorKind: LIT_ERROR.INVALID_ETH_BLOCKHASH.kind, - errorCode: LIT_ERROR.INVALID_ETH_BLOCKHASH.name, - }); + // -- we must also check for claim responses as a user may have submitted for a claim and signatures must be aggregated before returning + if (isSuccess && !hasSignedData && !hasClaimData) { + return mostCommonResponse as unknown as ExecuteJsResponse; } - const nonce = this.latestBlockhash!; - // -- (TRY) to get the wallet signature - let authSig = await this.#getWalletSig({ - authNeededCallback: params.authNeededCallback, - chain: params.chain || 'ethereum', - sessionCapabilityObject, - switchChain: params.switchChain, - expiration: expiration, - sessionKey: sessionKey, - sessionKeyUri: sessionKeyUri, - nonce, + // -- in the case where we are not signing anything on Lit action and using it as purely serverless function + if (!hasSignedData && !hasClaimData) { + return { + claims: {}, + signatures: null, + decryptions: [], + response: mostCommonResponse.response, + logs: mostCommonResponse.logs, + } as ExecuteJsNoSigningResponse; + } - // -- for recap - resourceAbilityRequests: params.resourceAbilityRequests, + // ========== Extract shares from response data ========== - // -- optional fields - ...(params.litActionCode && { litActionCode: params.litActionCode }), - ...(params.litActionIpfsId && { - litActionIpfsId: params.litActionIpfsId, - }), - ...(params.jsParams && { jsParams: params.jsParams }), + // -- 1. combine signed data as a list, and get the signatures from it + const signedDataList = responseData.map((r) => { + return removeDoubleQuotes(r.signedData); }); - const needToResignSessionKey = await this.#checkNeedToResignSessionKey({ - authSig, - sessionKeyUri, - resourceAbilityRequests: params.resourceAbilityRequests, - }); + logWithRequestId( + requestId, + 'signatures shares to combine: ', + signedDataList + ); - // -- (CHECK) if we need to resign the session key - if (needToResignSessionKey) { - log('need to re-sign session key. Signing...'); - authSig = await this.#authCallbackAndUpdateStorageItem({ - authCallback: params.authNeededCallback, - authCallbackParams: { - chain: params.chain || 'ethereum', - statement: sessionCapabilityObject.statement, - resources: [sessionCapabilityObject.encodeAsSiweResource()], - switchChain: params.switchChain, - expiration, - sessionKey: sessionKey, - uri: sessionKeyUri, - nonce, - resourceAbilityRequests: params.resourceAbilityRequests, + const signatures = getSignatures({ + requestId, + networkPubKeySet: this.networkPubKeySet, + minNodeCount: this.config.minNodeCount, + signedData: signedDataList, + }); - // -- optional fields - ...(params.litActionCode && { litActionCode: params.litActionCode }), - ...(params.litActionIpfsId && { - litActionIpfsId: params.litActionIpfsId, - }), - ...(params.jsParams && { jsParams: params.jsParams }), - }, - }); - } + // -- 2. combine responses as a string, and parse it as JSON if possible + const parsedResponse = parseAsJsonOrString(mostCommonResponse.response); - if ( - authSig.address === '' || - authSig.derivedVia === '' || - authSig.sig === '' || - authSig.signedMessage === '' - ) { - throwError({ - message: 'No wallet signature found', - errorKind: LIT_ERROR.WALLET_SIGNATURE_NOT_FOUND_ERROR.kind, - errorCode: LIT_ERROR.WALLET_SIGNATURE_NOT_FOUND_ERROR.name, - }); - // @ts-ignore - we throw an error above, so below should never be reached - return; - } + // -- 3. combine logs + const mostCommonLogs: string = mostCommonString( + responseData.map((r: NodeLog) => r.logs) + ); - // ===== AFTER we have Valid Signed Session Key ===== - // - Let's sign the resources with the session key - // - 5 minutes is the default expiration for a session signature - // - Because we can generate a new session sig every time the user wants to access a resource without prompting them to sign with their wallet - const sessionExpiration = - expiration ?? new Date(Date.now() + 1000 * 60 * 5).toISOString(); + // -- 4. combine claims + const claimsList = getClaimsList(responseData); + const claims = claimsList.length > 0 ? getClaims(claimsList) : undefined; - const capabilities = params.capacityDelegationAuthSig - ? [ - ...(params.capabilityAuthSigs ?? []), - params.capacityDelegationAuthSig, - authSig, - ] - : [...(params.capabilityAuthSigs ?? []), authSig]; - - const signingTemplate = { - sessionKey: sessionKey.publicKey, - resourceAbilityRequests: params.resourceAbilityRequests, - capabilities, - issuedAt: new Date().toISOString(), - expiration: sessionExpiration, + // ========== Result ========== + const returnVal: ExecuteJsResponse = { + claims, + signatures, + // decryptions: [], + response: parsedResponse, + logs: mostCommonLogs, }; - const signatures: SessionSigsMap = {}; - - this.connectedNodes.forEach((nodeAddress: string) => { - const toSign: SessionSigningTemplate = { - ...signingTemplate, - nodeAddress, - }; - - const signedMessage = JSON.stringify(toSign); - - const uint8arrayKey = uint8arrayFromString( - sessionKey.secretKey, - 'base16' - ); + log('returnVal:', returnVal); - const uint8arrayMessage = uint8arrayFromString(signedMessage, 'utf8'); - const signature = nacl.sign.detached(uint8arrayMessage, uint8arrayKey); + return returnVal; + }; - signatures[nodeAddress] = { - sig: uint8arrayToString(signature, 'base16'), - derivedVia: 'litSessionSignViaNacl', - signedMessage: signedMessage, - address: sessionKey.publicKey, - algo: 'ed25519', - }; + /** + * Generates a promise by sending a command to the Lit node + * + * @param url - The URL to send the command to. + * @param params - The parameters to include in the command. + * @param requestId - The ID of the request. + * @returns A promise that resolves with the response from the server. + */ + generatePromise = async ( + url: string, + params: any, + requestId: string + ): Promise => { + return await this.sendCommandToNode({ + url, + data: params, + requestId, }); - - log('signatures:', signatures); - - return signatures; }; /** - * Retrieves the PKP sessionSigs. + * Use PKP to sign * - * @param params - The parameters for retrieving the PKP sessionSigs. - * @returns A promise that resolves to the PKP sessionSigs. - * @throws An error if any of the required parameters are missing or if `litActionCode` and `ipfsId` exist at the same time. + * @param { JsonPkpSignSdkParams } params + * @param params.toSign - The data to sign + * @param params.pubKey - The public key to sign with + * @param params.sessionSigs - The session signatures to use + * @param params.authMethods - (optional) The auth methods to use */ - getPkpSessionSigs = async (params: GetPkpSessionSigs) => { - const chain = params?.chain || 'ethereum'; - - const pkpSessionSigs = this.getSessionSigs({ - chain, - ...params, - authNeededCallback: async (props: AuthCallbackParams) => { - // -- validate - if (!props.expiration) { - throw new Error( - '[getPkpSessionSigs/callback] expiration is required' - ); - } + pkpSign = async (params: JsonPkpSignSdkParams): Promise => { + // -- validate required params + const requiredParamKeys = ['toSign', 'pubKey']; - if (!props.resources) { - throw new Error('[getPkpSessionSigs/callback]resources is required'); - } + (requiredParamKeys as (keyof JsonPkpSignSdkParams)[]).forEach((key) => { + if (!params[key]) { + throwError({ + message: `"${key}" cannot be undefined, empty, or null. Please provide a valid value.`, + errorKind: LIT_ERROR.PARAM_NULL_ERROR.kind, + errorCode: LIT_ERROR.PARAM_NULL_ERROR.name, + }); + } + }); - if (!props.resourceAbilityRequests) { - throw new Error( - '[getPkpSessionSigs/callback]resourceAbilityRequests is required' - ); - } + // -- validate present of accepted auth methods + if ( + !params.sessionSigs && + (!params.authMethods || params.authMethods.length <= 0) + ) { + throwError({ + message: `Either sessionSigs or authMethods (length > 0) must be present.`, + errorKind: LIT_ERROR.PARAM_NULL_ERROR.kind, + errorCode: LIT_ERROR.PARAM_NULL_ERROR.name, + }); + } - // lit action code and ipfs id cannot exist at the same time - if (props.litActionCode && props.litActionIpfsId) { - throw new Error( - '[getPkpSessionSigs/callback]litActionCode and litActionIpfsId cannot exist at the same time' - ); - } + // ========== Get Node Promises ========== + // Handle promises for commands sent to Lit nodes + const wrapper = async ( + id: string + ): Promise | RejectedNodePromises> => { + const nodePromises = this.getNodePromises((url: string) => { + // -- get the session sig from the url key + const sessionSig = this.getSessionSigByUrl({ + sessionSigs: params.sessionSigs, + url, + }); - /** - * We must provide an empty array for authMethods even if we are not using any auth methods. - * So that the nodes can serialize the request correctly. - */ - const authMethods = params.authMethods || []; + const reqBody: JsonPkpSignRequest = { + toSign: normalizeArray(params.toSign), + pubkey: hexPrefixed(params.pubKey), + authSig: sessionSig, - const response = await this.signSessionKey({ - sessionKey: props.sessionKey, - statement: props.statement || 'Some custom statement.', - authMethods: [...authMethods], - pkpPublicKey: params.pkpPublicKey, - expiration: props.expiration, - resources: props.resources, - chainId: 1, + // -- optional params + ...(params.authMethods && + params.authMethods.length > 0 && { + authMethods: params.authMethods, + }), + }; - // -- required fields - resourceAbilityRequests: props.resourceAbilityRequests, + logWithRequestId(id, 'reqBody:', reqBody); - // -- optional fields - ...(props.litActionCode && { litActionCode: props.litActionCode }), - ...(props.litActionIpfsId && { - litActionIpfsId: props.litActionIpfsId, - }), - ...(props.jsParams && { jsParams: props.jsParams }), + const urlWithPath = composeLitUrl({ + url, + endpoint: LIT_ENDPOINT.PKP_SIGN, }); - return response.authSig; + return this.generatePromise(urlWithPath, reqBody, id); + }); + + const res = await this.handleNodePromises( + nodePromises, + id, + this.connectedNodes.size // ECDSA requires responses from all nodes, but only shares from minNodeCount. + ); + return res; + }; // wrapper end + + // ========== Execute with Retry ========== + const res = await executeWithRetry< + RejectedNodePromises | SuccessNodePromises + >( + wrapper, + (error: any, requestId: string, isFinal: boolean) => { + if (!isFinal) { + logError('errror occured, retrying operation'); + } }, - }); + this.config.retryTolerance + ); - return pkpSessionSigs; - }; + // ========== Handle Response ========== + const requestId = res.requestId; - /** - * Retrieves session signatures specifically for Lit Actions. - * Unlike `getPkpSessionSigs`, this function requires either `litActionCode` or `litActionIpfsId`, and `jsParams` must be provided. - * - * @param params - The parameters required for retrieving the session signatures. - * @returns A promise that resolves with the session signatures. - */ - getLitActionSessionSigs = async (params: GetLitActionSessionSigs) => { - // Check if either litActionCode or litActionIpfsId is provided - if (!params.litActionCode && !params.litActionIpfsId) { - throw new Error( - "Either 'litActionCode' or 'litActionIpfsId' must be provided." - ); + // -- case: promises rejected + if (!res.success) { + this._throwNodeError(res as RejectedNodePromises, requestId); } - // Check if jsParams is provided - if (!params.jsParams) { - throw new Error("'jsParams' is required."); - } + // -- case: promises success (TODO: check the keys of "values") + const responseData = (res as SuccessNodePromises).values; - return this.getPkpSessionSigs(params); + logWithRequestId( + requestId, + 'responseData', + JSON.stringify(responseData, null, 2) + ); + + // ========== Extract shares from response data ========== + // -- 1. combine signed data as a list, and get the signatures from it + const signedDataList = parsePkpSignResponse(responseData); + + const signatures = getSignatures<{ signature: SigResponse }>({ + requestId, + networkPubKeySet: this.networkPubKeySet, + minNodeCount: this.config.minNodeCount, + signedData: signedDataList, + }); + + logWithRequestId(requestId, `signature combination`, signatures); + + return signatures.signature; // only a single signature is ever present, so we just return it. }; - /** ============================== END POINTS ============================== */ /** - * Sign a session public key using a PKP, which generates an authSig. - * Endpoint: /web/sign_session_key endpoint. - * @returns {Object} An object containing the resulting signature. + * + * Request a signed JWT from the LIT network. Before calling this function, you must know the access control conditions for the item you wish to gain authorization for. + * + * @param { GetSignedTokenRequest } params + * + * @returns { Promise } final JWT + * */ - signSessionKey = async ( - params: SignSessionKeyProp - ): Promise => { - log(`[signSessionKey] params:`, params); + getSignedToken = async (params: GetSignedTokenRequest): Promise => { + // ========== Prepare Params ========== + const { chain, authSig, sessionSigs } = params; - // ========== Validate Params ========== - // -- validate: If it's NOT ready + // ========== Validation ========== + // -- validate if it's ready if (!this.ready) { const message = - '[signSessionKey] ]LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; - + '3 LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; throwError({ message, errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, @@ -1252,114 +1305,85 @@ const resourceAbilityRequests = [ }); } - // -- construct SIWE message that will be signed by node to generate an authSig. - const _expiration = - params.expiration || - new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); - - // Try to get it from local storage, if not generates one~ - const sessionKey: SessionKeyPair = - params.sessionKey ?? this.getSessionKey(); - const sessionKeyUri = LIT_SESSION_KEY_URI + sessionKey.publicKey; - - log( - `[signSessionKey] sessionKeyUri is not found in params, generating a new one`, - sessionKeyUri - ); - - if (!sessionKeyUri) { - throw new Error( - '[signSessionKey] sessionKeyUri is not defined. Please provide a sessionKeyUri or a sessionKey.' - ); + // -- validate if this.networkPubKeySet is null + if (this.networkPubKeySet === null) { + return throwError({ + message: 'networkPubKeySet cannot be null', + errorKind: LIT_ERROR.PARAM_NULL_ERROR.kind, + errorCode: LIT_ERROR.PARAM_NULL_ERROR.name, + }); } - // Compute the address from the public key if it's provided. Otherwise, the node will compute it. - const pkpEthAddress = (function () { - // prefix '0x' if it's not already prefixed - params.pkpPublicKey = hexPrefixed(params.pkpPublicKey!); - - if (params.pkpPublicKey) return computeAddress(params.pkpPublicKey); - - // This will be populated by the node, using dummy value for now. - return '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; - })(); + const paramsIsSafe = safeParams({ + functionName: 'getSignedToken', + params, + }); - let siwe_statement = 'Lit Protocol PKP session signature'; - if (params.statement) { - siwe_statement += ' ' + params.statement; - log(`[signSessionKey] statement found in params: "${params.statement}"`); + if (!paramsIsSafe) { + return throwError({ + message: `Parameter validation failed.`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + }); } - let siweMessage; + // ========== Prepare ========== + // we need to send jwt params iat (issued at) and exp (expiration) + // because the nodes may have different wall clock times + // the nodes will verify that these params are withing a grace period + const { iat, exp } = this.getJWTParams(); - const siweParams = { - domain: params?.domain || globalThis.location?.host || 'litprotocol.com', - walletAddress: pkpEthAddress, - statement: siwe_statement, - uri: sessionKeyUri, - version: '1', - chainId: params.chainId ?? 1, - expiration: _expiration, - nonce: this.latestBlockhash!, - }; + // ========== Formatting Access Control Conditions ========= + const { + error, + formattedAccessControlConditions, + formattedEVMContractConditions, + formattedSolRpcConditions, + formattedUnifiedAccessControlConditions, + }: FormattedMultipleAccs = this.getFormattedAccessControlConditions(params); - if (params.resourceAbilityRequests) { - siweMessage = await createSiweMessageWithRecaps({ - ...siweParams, - resources: params.resourceAbilityRequests, - litNodeClient: this, + if (error) { + return throwError({ + message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, }); - } else { - siweMessage = await createSiweMessage(siweParams); } // ========== Get Node Promises ========== - // -- fetch shares from nodes - const body: JsonSignSessionKeyRequestV1 = { - sessionKey: sessionKeyUri, - authMethods: params.authMethods, - ...(params?.pkpPublicKey && { pkpPublicKey: params.pkpPublicKey }), - siweMessage: siweMessage, - curveType: LIT_CURVE.BLS, - - // -- custom auths - ...(params?.litActionIpfsId && { - litActionIpfsId: params.litActionIpfsId, - }), - ...(params?.litActionCode && { code: params.litActionCode }), - ...(params?.jsParams && { jsParams: params.jsParams }), - ...(this.currentEpochNumber && { epoch: this.currentEpochNumber }), - }; - - log(`[signSessionKey] body:`, body); - const wrapper = async ( id: string ): Promise | RejectedNodePromises> => { - logWithRequestId(id, 'signSessionKey body', body); - const nodePromises = this._getNodePromises((url: string) => { - const reqBody: JsonSignSessionKeyRequestV1 = body; + const nodePromises = this.getNodePromises((url: string) => { + // -- if session key is available, use it + const authSigToSend = sessionSigs ? sessionSigs[url] : authSig; + + const reqBody: SigningAccessControlConditionRequest = { + accessControlConditions: formattedAccessControlConditions, + evmContractConditions: formattedEVMContractConditions, + solRpcConditions: formattedSolRpcConditions, + unifiedAccessControlConditions: + formattedUnifiedAccessControlConditions, + chain, + authSig: authSigToSend, + iat, + exp, + }; const urlWithPath = composeLitUrl({ url, - endpoint: LIT_ENDPOINT.SIGN_SESSION_KEY, + endpoint: LIT_ENDPOINT.SIGN_ACCS, }); - return this.#generatePromise(urlWithPath, reqBody, id); + return this.generatePromise(urlWithPath, reqBody, id); }); // -- resolve promises - let res; - try { - res = await this._handleNodePromises( - nodePromises, - id, - this.connectedNodes.size - ); - log('signSessionKey node promises:', res); - } catch (e) { - throw new Error(`Error when handling node promises: ${e}`); - } + const res = await this.handleNodePromises( + nodePromises, + id, + this.config.minNodeCount + ); return res; }; @@ -1367,179 +1391,148 @@ const resourceAbilityRequests = [ RejectedNodePromises | SuccessNodePromises >( wrapper, - (_error: any, _requestId: string, isFinal: boolean) => { + (error: any, requestId: string, isFinal: boolean) => { if (!isFinal) { logError('an error occured, attempting to retry '); } }, this.config.retryTolerance ); - const requestId = res.requestId; - logWithRequestId(requestId, 'handleNodePromises res:', res); // -- case: promises rejected - if (!this.#isSuccessNodePromises(res)) { + if (res.success === false) { this._throwNodeError(res as RejectedNodePromises, requestId); - return {} as SignSessionKeyResponse; } - const responseData: BlsResponseData[] = res.values; - logWithRequestId( - requestId, - '[signSessionKey] responseData', - JSON.stringify(responseData, null, 2) + const signatureShares: NodeBlsSigningShare[] = ( + res as SuccessNodePromises + ).values; + + log('signatureShares', signatureShares); + + // ========== Result ========== + const finalJwt: string = this.combineSharesAndGetJWT( + signatureShares, + requestId ); - // ========== Extract shares from response data ========== - // -- 1. combine signed data as a list, and get the signatures from it - let curveType = responseData[0]?.curveType; + return finalJwt; + }; - if (!curveType) { - log(`[signSessionKey] curveType not found. Defaulting to ECDSA.`); - curveType = 'ECDSA'; + /** + * + * Encrypt data using the LIT network public key. + * + * @param { EncryptSdkParams } params + * @param params.dataToEncrypt - The data to encrypt + * @param params.accessControlConditions - (optional) The access control conditions for the data + * @param params.evmContractConditions - (optional) The EVM contract conditions for the data + * @param params.solRpcConditions - (optional) The Solidity RPC conditions for the data + * @param params.unifiedAccessControlConditions - (optional) The unified access control conditions for the data + * + * @return { Promise } The encrypted ciphertext and the hash of the data + * + * @throws { Error } if the LIT node client is not ready + * @throws { Error } if the subnetPubKey is null + */ + encrypt = async (params: EncryptSdkParams): Promise => { + // ========== Validate Params ========== + // -- validate if it's ready + if (!this.ready) { + const message = + '6 LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; + throwError({ + message, + errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, + errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, + }); } - log(`[signSessionKey] curveType is "${curveType}"`); + // -- validate if this.subnetPubKey is null + if (!this.subnetPubKey) { + const message = 'subnetPubKey cannot be null'; + return throwError({ + message, + errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, + errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, + }); + } - let signedDataList = responseData.map((s) => s.dataSigned); + const paramsIsSafe = safeParams({ + functionName: 'encrypt', + params, + }); - if (signedDataList.length <= 0) { - const err = `[signSessionKey] signedDataList is empty.`; - log(err); - throw new Error(err); + if (!paramsIsSafe) { + return throwError({ + message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + }); } - logWithRequestId( - requestId, - '[signSessionKey] signedDataList', - signedDataList - ); + // ========== Validate Access Control Conditions Schema ========== + await this.validateAccessControlConditionsSchema(params); - // -- checking if we have enough shares - const validatedSignedDataList = responseData - .map((data: BlsResponseData) => { - // each of this field cannot be empty - let requiredFields = [ - 'signatureShare', - 'curveType', - 'shareIndex', - 'siweMessage', - 'dataSigned', - 'blsRootPubkey', - 'result', - ]; + // ========== Hashing Access Control Conditions ========= + // hash the access control conditions + const hashOfConditions: ArrayBuffer | undefined = + await this.getHashedAccessControlConditions(params); - // check if all required fields are present - for (const field of requiredFields) { - const key: keyof BlsResponseData = field as keyof BlsResponseData; + if (!hashOfConditions) { + return throwError({ + message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + }); + } - if (!data[key] || data[key] === '') { - log( - `[signSessionKey] Invalid signed data. "${field}" is missing. Not a problem, we only need ${this.config.minNodeCount} nodes to sign the session key.` - ); - return null; - } - } - - if (!data.signatureShare.ProofOfPossession) { - const err = `[signSessionKey] Invalid signed data. "ProofOfPossession" is missing.`; - log(err); - throw new Error(err); - } - - return data; - }) - .filter((item) => item !== null); - - logWithRequestId( - requestId, - '[signSessionKey] requested length:', - signedDataList.length - ); - logWithRequestId( - requestId, - '[signSessionKey] validated length:', - validatedSignedDataList.length - ); - logWithRequestId( - requestId, - '[signSessionKey] minimum required length:', - this.config.minNodeCount + const hashOfConditionsStr = uint8arrayToString( + new Uint8Array(hashOfConditions), + 'base16' ); - if (validatedSignedDataList.length < this.config.minNodeCount) { - throw new Error( - `[signSessionKey] not enough nodes signed the session key. Expected ${this.config.minNodeCount}, got ${validatedSignedDataList.length}` - ); - } - - const blsSignedData: BlsResponseData[] = - validatedSignedDataList as BlsResponseData[]; - - const sigType = mostCommonString(blsSignedData.map((s) => s.curveType)); - log(`[signSessionKey] sigType:`, sigType); - - const signatureShares = getBlsSignatures(blsSignedData); - log(`[signSessionKey] signatureShares:`, signatureShares); - - const blsCombinedSignature = blsSdk.combine_signature_shares( - signatureShares.map((s) => JSON.stringify(s)) + // ========== Hashing Private Data ========== + // hash the private data + const hashOfPrivateData = await crypto.subtle.digest( + 'SHA-256', + params.dataToEncrypt ); - - log(`[signSessionKey] blsCombinedSignature:`, blsCombinedSignature); - - const publicKey = removeHexPrefix(params.pkpPublicKey); - log(`[signSessionKey] publicKey:`, publicKey); - - const dataSigned = mostCommonString( - blsSignedData.map((s: any) => s.dataSigned) + const hashOfPrivateDataStr = uint8arrayToString( + new Uint8Array(hashOfPrivateData), + 'base16' ); - log(`[signSessionKey] dataSigned:`, dataSigned); - const mostCommonSiweMessage = mostCommonString( - blsSignedData.map((s: any) => s.siweMessage) + // ========== Assemble identity parameter ========== + const identityParam = this.#getIdentityParamForEncryption( + hashOfConditionsStr, + hashOfPrivateDataStr ); - log(`[signSessionKey] mostCommonSiweMessage:`, mostCommonSiweMessage); - - const signedMessage = normalizeAndStringify(mostCommonSiweMessage); - - log(`[signSessionKey] signedMessage:`, signedMessage); - - const signSessionKeyRes: SignSessionKeyResponse = { - authSig: { - sig: JSON.stringify({ - ProofOfPossession: blsCombinedSignature, - }), - algo: 'LIT_BLS', - derivedVia: 'lit.bls', - signedMessage, - address: computeAddress(hexPrefixed(publicKey)), - }, - pkpPublicKey: publicKey, - }; + // ========== Encrypt ========== + const ciphertext = encrypt( + this.subnetPubKey, + params.dataToEncrypt, + uint8arrayFromString(identityParam, 'utf8') + ); - return signSessionKeyRes; + return { ciphertext, dataToEncryptHash: hashOfPrivateDataStr }; }; /** * - * Execute JS on the nodes and combine and return any resulting signatures - * Endpoint: /web/execute - * @param { JsonExecutionSdkParams } params - * - * @returns { ExecuteJsResponse } + * Decrypt ciphertext with the LIT network. * */ - executeJs = async ( - params: JsonExecutionSdkParams - ): Promise => { + decrypt = async (params: DecryptRequest): Promise => { + const { sessionSigs, chain, ciphertext, dataToEncryptHash } = params; + // ========== Validate Params ========== + // -- validate if it's ready if (!this.ready) { const message = - '[executeJs] LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; - + '6 LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; throwError({ message, errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, @@ -1547,592 +1540,816 @@ const resourceAbilityRequests = [ }); } + // -- validate if this.subnetPubKey is null + if (!this.subnetPubKey) { + const message = 'subnetPubKey cannot be null'; + return throwError({ + message, + errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, + errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, + }); + } + const paramsIsSafe = safeParams({ - functionName: 'executeJs', - params: params, + functionName: 'decrypt', + params, }); if (!paramsIsSafe) { return throwError({ - message: 'executeJs params are not valid', - errorKind: LIT_ERROR.INVALID_PARAM_TYPE.kind, - errorCode: LIT_ERROR.INVALID_PARAM_TYPE.name, + message: `Parameter validation failed.`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, }); } - // Format the params - const formattedParams: JsonExecutionSdkParams = { - ...params, - ...(params.jsParams && { jsParams: normalizeJsParams(params.jsParams) }), - ...(params.code && { code: encodeCode(params.code) }), - }; + // ========== Hashing Access Control Conditions ========= + // hash the access control conditions + const hashOfConditions: ArrayBuffer | undefined = + await this.getHashedAccessControlConditions(params); - // ========== Get Node Promises ========== - // Handle promises for commands sent to Lit nodes + if (!hashOfConditions) { + return throwError({ + message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + }); + } + + const hashOfConditionsStr = uint8arrayToString( + new Uint8Array(hashOfConditions), + 'base16' + ); + + // ========== Formatting Access Control Conditions ========= + const { + error, + formattedAccessControlConditions, + formattedEVMContractConditions, + formattedSolRpcConditions, + formattedUnifiedAccessControlConditions, + }: FormattedMultipleAccs = this.getFormattedAccessControlConditions(params); + + if (error) { + throwError({ + message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + }); + } + + // ========== Assemble identity parameter ========== + const identityParam = this.#getIdentityParamForEncryption( + hashOfConditionsStr, + dataToEncryptHash + ); + + log('identityParam', identityParam); + + // ========== Get Network Signature ========== const wrapper = async ( - requestId: string + id: string ): Promise | RejectedNodePromises> => { - const nodePromises = this._getNodePromises(async (url: string) => { - // -- choose the right signature - const sessionSig = this._getSessionSigByUrl({ - sessionSigs: formattedParams.sessionSigs, - url, - }); + const nodePromises = this.getNodePromises((url: string) => { + // -- if session key is available, use it + const authSigToSend = sessionSigs ? sessionSigs[url] : params.authSig; - const reqBody: JsonExecutionRequest = { - ...formattedParams, - authSig: sessionSig, + if (!authSigToSend) { + return throwError({ + message: `authSig is required`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + }); + } + + const reqBody: EncryptionSignRequest = { + accessControlConditions: formattedAccessControlConditions, + evmContractConditions: formattedEVMContractConditions, + solRpcConditions: formattedSolRpcConditions, + unifiedAccessControlConditions: + formattedUnifiedAccessControlConditions, + dataToEncryptHash, + chain, + authSig: authSigToSend, + epoch: this.currentEpochNumber!, }; - const urlWithPath = composeLitUrl({ + const urlWithParh = composeLitUrl({ url, - endpoint: LIT_ENDPOINT.EXECUTE_JS, + endpoint: LIT_ENDPOINT.ENCRYPTION_SIGN, }); - return this.#generatePromise(urlWithPath, reqBody, requestId); + return this.generatePromise(urlWithParh, reqBody, id); }); // -- resolve promises - const res = await this._handleNodePromises( + const res = await this.handleNodePromises( nodePromises, - requestId, - this.connectedNodes.size + id, + this.config.minNodeCount ); - return res; - }; // wrapper end + }; - // ========== Execute with Retry ========== const res = await executeWithRetry< RejectedNodePromises | SuccessNodePromises >( wrapper, - (error: any, requestId: string, isFinal: boolean) => { - logError('an error occured, attempting to retry operation'); + (_error: string, _requestId: string, _isFinal: boolean) => { + logError('an error occured attempting to retry'); }, this.config.retryTolerance ); - // ========== Handle Response ========== const requestId = res.requestId; // -- case: promises rejected - if (!res.success) { + if (res.success === false) { this._throwNodeError(res as RejectedNodePromises, requestId); } - // -- case: promises success (TODO: check the keys of "values") - const responseData = (res as SuccessNodePromises).values; + const signatureShares: NodeBlsSigningShare[] = ( + res as SuccessNodePromises + ).values; - logWithRequestId( - requestId, - 'executeJs responseData from node : ', - JSON.stringify(responseData, null, 2) + logWithRequestId(requestId, 'signatureShares', signatureShares); + + // ========== Result ========== + const decryptedData = this.#decryptWithSignatureShares( + this.subnetPubKey, + uint8arrayFromString(identityParam, 'utf8'), + ciphertext, + signatureShares ); - // -- find the responseData that has the most common response - const mostCommonResponse = findMostCommonResponse( - responseData - ) as NodeShare; + return { decryptedData }; + }; - const responseFromStrategy: any = processLitActionResponseStrategy( - responseData, - params.responseStrategy ?? { strategy: 'leastCommon' } + getLitResourceForEncryption = async ( + params: EncryptRequest + ): Promise => { + // ========== Hashing Access Control Conditions ========= + // hash the access control conditions + const hashOfConditions: ArrayBuffer | undefined = + await this.getHashedAccessControlConditions(params); + + if (!hashOfConditions) { + return throwError({ + message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, + errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, + errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + }); + } + + const hashOfConditionsStr = uint8arrayToString( + new Uint8Array(hashOfConditions), + 'base16' ); - mostCommonResponse.response = responseFromStrategy; - const isSuccess = mostCommonResponse.success; - const hasSignedData = Object.keys(mostCommonResponse.signedData).length > 0; - const hasClaimData = Object.keys(mostCommonResponse.claimData).length > 0; + // ========== Hashing Private Data ========== + // hash the private data + const hashOfPrivateData = await crypto.subtle.digest( + 'SHA-256', + params.dataToEncrypt + ); + const hashOfPrivateDataStr = uint8arrayToString( + new Uint8Array(hashOfPrivateData), + 'base16' + ); - // -- we must also check for claim responses as a user may have submitted for a claim and signatures must be aggregated before returning - if (isSuccess && !hasSignedData && !hasClaimData) { - return mostCommonResponse as unknown as ExecuteJsResponse; - } + return new LitAccessControlConditionResource( + `${hashOfConditionsStr}/${hashOfPrivateDataStr}` + ); + }; - // -- in the case where we are not signing anything on Lit action and using it as purely serverless function - if (!hasSignedData && !hasClaimData) { - return { - claims: {}, - signatures: null, - decryptions: [], - response: mostCommonResponse.response, - logs: mostCommonResponse.logs, - } as ExecuteJsNoSigningResponse; - } + #getIdentityParamForEncryption = ( + hashOfConditionsStr: string, + hashOfPrivateDataStr: string + ): string => { + return new LitAccessControlConditionResource( + `${hashOfConditionsStr}/${hashOfPrivateDataStr}` + ).getResourceKey(); + }; - // ========== Extract shares from response data ========== + /** ============================== SESSION ============================== */ - // -- 1. combine signed data as a list, and get the signatures from it - const signedDataList = responseData.map((r) => { - return removeDoubleQuotes(r.signedData); - }); + /** + * Sign a session public key using a PKP, which generates an authSig. + * @returns {Object} An object containing the resulting signature. + */ - logWithRequestId( - requestId, - 'signatures shares to combine: ', - signedDataList - ); + signSessionKey = async ( + params: SignSessionKeyProp + ): Promise => { + log(`[signSessionKey] params:`, params); - const signatures = getSignatures({ - requestId, - networkPubKeySet: this.networkPubKeySet, - minNodeCount: this.config.minNodeCount, - signedData: signedDataList, - }); + // ========== Validate Params ========== + // -- validate: If it's NOT ready + if (!this.ready) { + const message = + '[signSessionKey] ]LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; - // -- 2. combine responses as a string, and parse it as JSON if possible - const parsedResponse = parseAsJsonOrString(mostCommonResponse.response); + throwError({ + message, + errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, + errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, + }); + } - // -- 3. combine logs - const mostCommonLogs: string = mostCommonString( - responseData.map((r: NodeLog) => r.logs) + // -- construct SIWE message that will be signed by node to generate an authSig. + const _expiration = + params.expiration || + new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); + + // Try to get it from local storage, if not generates one~ + const sessionKey: SessionKeyPair = + params.sessionKey ?? this.getSessionKey(); + const sessionKeyUri = LIT_SESSION_KEY_URI + sessionKey.publicKey; + + log( + `[signSessionKey] sessionKeyUri is not found in params, generating a new one`, + sessionKeyUri ); - // -- 4. combine claims - const claimsList = getClaimsList(responseData); - const claims = claimsList.length > 0 ? getClaims(claimsList) : undefined; + if (!sessionKeyUri) { + throw new Error( + '[signSessionKey] sessionKeyUri is not defined. Please provide a sessionKeyUri or a sessionKey.' + ); + } - // ========== Result ========== - const returnVal: ExecuteJsResponse = { - claims, - signatures, - // decryptions: [], - response: parsedResponse, - logs: mostCommonLogs, - }; + // Compute the address from the public key if it's provided. Otherwise, the node will compute it. + const pkpEthAddress = (function () { + // prefix '0x' if it's not already prefixed + params.pkpPublicKey = hexPrefixed(params.pkpPublicKey!); - log('returnVal:', returnVal); + if (params.pkpPublicKey) return computeAddress(params.pkpPublicKey); - return returnVal; - }; + // This will be populated by the node, using dummy value for now. + return '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; + })(); - /** - * Use PKP to sign - * - * Endpoint: /web/pkp/sign - * - * @param { JsonPkpSignSdkParams } params - * @param params.toSign - The data to sign - * @param params.pubKey - The public key to sign with - * @param params.sessionSigs - The session signatures to use - * @param params.authMethods - (optional) The auth methods to use - */ - pkpSign = async (params: JsonPkpSignSdkParams): Promise => { - // -- validate required params - const requiredParamKeys = ['toSign', 'pubKey']; + let siwe_statement = 'Lit Protocol PKP session signature'; + if (params.statement) { + siwe_statement += ' ' + params.statement; + log(`[signSessionKey] statement found in params: "${params.statement}"`); + } - (requiredParamKeys as (keyof JsonPkpSignSdkParams)[]).forEach((key) => { - if (!params[key]) { - throwError({ - message: `"${key}" cannot be undefined, empty, or null. Please provide a valid value.`, - errorKind: LIT_ERROR.PARAM_NULL_ERROR.kind, - errorCode: LIT_ERROR.PARAM_NULL_ERROR.name, - }); - } - }); + let siweMessage; - // -- validate present of accepted auth methods - if ( - !params.sessionSigs && - (!params.authMethods || params.authMethods.length <= 0) - ) { - throwError({ - message: `Either sessionSigs or authMethods (length > 0) must be present.`, - errorKind: LIT_ERROR.PARAM_NULL_ERROR.kind, - errorCode: LIT_ERROR.PARAM_NULL_ERROR.name, + const siweParams = { + domain: params?.domain || globalThis.location?.host || 'litprotocol.com', + walletAddress: pkpEthAddress, + statement: siwe_statement, + uri: sessionKeyUri, + version: '1', + chainId: params.chainId ?? 1, + expiration: _expiration, + nonce: this.latestBlockhash!, + }; + + if (params.resourceAbilityRequests) { + siweMessage = await createSiweMessageWithRecaps({ + ...siweParams, + resources: params.resourceAbilityRequests, + litNodeClient: this, }); + } else { + siweMessage = await createSiweMessage(siweParams); } // ========== Get Node Promises ========== - // Handle promises for commands sent to Lit nodes - const wrapper = async ( - id: string - ): Promise | RejectedNodePromises> => { - const nodePromises = this._getNodePromises((url: string) => { - // -- get the session sig from the url key - const sessionSig = this._getSessionSigByUrl({ - sessionSigs: params.sessionSigs, - url, - }); + // -- fetch shares from nodes + const body: JsonSignSessionKeyRequestV1 = { + sessionKey: sessionKeyUri, + authMethods: params.authMethods, + ...(params?.pkpPublicKey && { pkpPublicKey: params.pkpPublicKey }), + siweMessage: siweMessage, + curveType: LIT_CURVE.BLS, - const reqBody: JsonPkpSignRequest = { - toSign: normalizeArray(params.toSign), - pubkey: hexPrefixed(params.pubKey), - authSig: sessionSig, + // -- custom auths + ...(params?.litActionIpfsId && { + litActionIpfsId: params.litActionIpfsId, + }), + ...(params?.litActionCode && { code: params.litActionCode }), + ...(params?.jsParams && { jsParams: params.jsParams }), + ...(this.currentEpochNumber && { epoch: this.currentEpochNumber }), + }; - // -- optional params - ...(params.authMethods && - params.authMethods.length > 0 && { - authMethods: params.authMethods, - }), - }; + log(`[signSessionKey] body:`, body); - logWithRequestId(id, 'reqBody:', reqBody); + const wrapper = async ( + id: string + ): Promise | RejectedNodePromises> => { + logWithRequestId(id, 'signSessionKey body', body); + const nodePromises = this.getNodePromises((url: string) => { + const reqBody: JsonSignSessionKeyRequestV1 = body; const urlWithPath = composeLitUrl({ url, - endpoint: LIT_ENDPOINT.PKP_SIGN, + endpoint: LIT_ENDPOINT.SIGN_SESSION_KEY, }); - return this.#generatePromise(urlWithPath, reqBody, id); + return this.generatePromise(urlWithPath, reqBody, id); }); - const res = await this._handleNodePromises( - nodePromises, - id, - this.connectedNodes.size // ECDSA requires responses from all nodes, but only shares from minNodeCount. - ); + // -- resolve promises + let res; + try { + res = await this.handleNodePromises( + nodePromises, + id, + this.connectedNodes.size + ); + log('signSessionKey node promises:', res); + } catch (e) { + throw new Error(`Error when handling node promises: ${e}`); + } return res; - }; // wrapper end + }; - // ========== Execute with Retry ========== const res = await executeWithRetry< RejectedNodePromises | SuccessNodePromises >( wrapper, - (error: any, requestId: string, isFinal: boolean) => { + (_error: any, _requestId: string, isFinal: boolean) => { if (!isFinal) { - logError('errror occured, retrying operation'); + logError('an error occured, attempting to retry '); } }, this.config.retryTolerance ); - // ========== Handle Response ========== const requestId = res.requestId; + logWithRequestId(requestId, 'handleNodePromises res:', res); // -- case: promises rejected - if (!res.success) { + if (!this.#isSuccessNodePromises(res)) { this._throwNodeError(res as RejectedNodePromises, requestId); + return {} as SignSessionKeyResponse; } - // -- case: promises success (TODO: check the keys of "values") - const responseData = (res as SuccessNodePromises).values; - + const responseData: BlsResponseData[] = res.values; logWithRequestId( requestId, - 'responseData', + '[signSessionKey] responseData', JSON.stringify(responseData, null, 2) ); // ========== Extract shares from response data ========== // -- 1. combine signed data as a list, and get the signatures from it - const signedDataList = parsePkpSignResponse(responseData); - - const signatures = getSignatures<{ signature: SigResponse }>({ - requestId, - networkPubKeySet: this.networkPubKeySet, - minNodeCount: this.config.minNodeCount, - signedData: signedDataList, - }); + let curveType = responseData[0]?.curveType; - logWithRequestId(requestId, `signature combination`, signatures); + if (!curveType) { + log(`[signSessionKey] curveType not found. Defaulting to ECDSA.`); + curveType = 'ECDSA'; + } - return signatures.signature; // only a single signature is ever present, so we just return it. - }; + log(`[signSessionKey] curveType is "${curveType}"`); - /** - * Authenticates an Auth Method for claiming a Programmable Key Pair (PKP). - * A {@link MintCallback} can be defined for custom on chain interactions - * by default the callback will forward to a relay server for minting on chain. - * - * Endpoint: /web/pkp/claim - * - * @param {ClaimKeyRequest} params an Auth Method and {@link MintCallback} - * @returns {Promise} - */ - async claimKeyId( - params: ClaimRequest - ): Promise { - if (!this.ready) { - const message = - 'LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; - throwError({ - message, - errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, - errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, - }); - } + let signedDataList = responseData.map((s) => s.dataSigned); - if (params.authMethod.authMethodType == AuthMethodType.WebAuthn) { - throwError({ - message: - 'Unsupported auth method type. Webauthn, and Lit Actions are not supported for claiming', - errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, - errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, - }); + if (signedDataList.length <= 0) { + const err = `[signSessionKey] signedDataList is empty.`; + log(err); + throw new Error(err); } - let requestId; - const wrapper = async ( - id: string - ): Promise | RejectedNodePromises> => { - const nodePromises = this._getNodePromises((url: string) => { - if (!params.authMethod) { - throw new Error('authMethod is required'); - } - - const reqBody: JsonPKPClaimKeyRequest = { - authMethod: params.authMethod, - }; - const urlWithPath = composeLitUrl({ - url, - endpoint: LIT_ENDPOINT.PKP_CLAIM, - }); + logWithRequestId( + requestId, + '[signSessionKey] signedDataList', + signedDataList + ); - return this.#generatePromise(urlWithPath, reqBody, id); - }); + // -- checking if we have enough shares + const validatedSignedDataList = responseData + .map((data: BlsResponseData) => { + // each of this field cannot be empty + let requiredFields = [ + 'signatureShare', + 'curveType', + 'shareIndex', + 'siweMessage', + 'dataSigned', + 'blsRootPubkey', + 'result', + ]; - const responseData = await this._handleNodePromises( - nodePromises, - id, - this.connectedNodes.size - ); + // check if all required fields are present + for (const field of requiredFields) { + const key: keyof BlsResponseData = field as keyof BlsResponseData; - return responseData; - }; + if (!data[key] || data[key] === '') { + log( + `[signSessionKey] Invalid signed data. "${field}" is missing. Not a problem, we only need ${this.config.minNodeCount} nodes to sign the session key.` + ); + return null; + } + } - const responseData = await executeWithRetry< - RejectedNodePromises | SuccessNodePromises - >( - wrapper, - (_error: any, _requestId: string, isFinal: boolean) => { - if (!isFinal) { - logError('an error occured, attempting to retry'); + if (!data.signatureShare.ProofOfPossession) { + const err = `[signSessionKey] Invalid signed data. "ProofOfPossession" is missing.`; + log(err); + throw new Error(err); } - }, - this.config.retryTolerance - ); - requestId = responseData.requestId; - if (responseData.success === true) { - const nodeSignatures: Signature[] = ( - responseData as SuccessNodePromises - ).values.map((r: any) => { - const sig = ethers.utils.splitSignature(`0x${r.signature}`); - return { - r: sig.r, - s: sig.s, - v: sig.v, - }; - }); + return data; + }) + .filter((item) => item !== null); - logWithRequestId( - requestId, - `responseData: ${JSON.stringify(responseData, null, 2)}` + logWithRequestId( + requestId, + '[signSessionKey] requested length:', + signedDataList.length + ); + logWithRequestId( + requestId, + '[signSessionKey] validated length:', + validatedSignedDataList.length + ); + logWithRequestId( + requestId, + '[signSessionKey] minimum required length:', + this.config.minNodeCount + ); + if (validatedSignedDataList.length < this.config.minNodeCount) { + throw new Error( + `[signSessionKey] not enough nodes signed the session key. Expected ${this.config.minNodeCount}, got ${validatedSignedDataList.length}` ); + } - const derivedKeyId = (responseData as SuccessNodePromises).values[0] - .derivedKeyId; + const blsSignedData: BlsResponseData[] = + validatedSignedDataList as BlsResponseData[]; - const pubkey: string = this.computeHDPubKey(derivedKeyId); - logWithRequestId( - requestId, - `pubkey ${pubkey} derived from key id ${derivedKeyId}` - ); + const sigType = mostCommonString(blsSignedData.map((s) => s.curveType)); + log(`[signSessionKey] sigType:`, sigType); - const relayParams: ClaimRequest<'relay'> = - params as ClaimRequest<'relay'>; + const signatureShares = getBlsSignatures(blsSignedData); - let mintTx = ''; - if (params.mintCallback && 'signer' in params) { - mintTx = await params.mintCallback( - { - derivedKeyId, - authMethodType: params.authMethod.authMethodType, - signatures: nodeSignatures, - pubkey, - signer: (params as ClaimRequest<'client'>).signer, - ...relayParams, - }, - this.config.litNetwork as LitNetwork - ); - } else { - mintTx = await defaultMintClaimCallback( - { - derivedKeyId, - authMethodType: params.authMethod.authMethodType, - signatures: nodeSignatures, - pubkey, - ...relayParams, - }, - this.config.litNetwork as LitNetwork - ); - } + log(`[signSessionKey] signatureShares:`, signatureShares); - return { - signatures: nodeSignatures, - claimedKeyId: derivedKeyId, - pubkey, - mintTx, - }; - } else { - return throwError({ - message: `Claim request has failed. Request trace id: lit_${requestId} `, - errorKind: LIT_ERROR.UNKNOWN_ERROR.kind, - errorCode: LIT_ERROR.UNKNOWN_ERROR.code, - }); - } - } + const blsCombinedSignature = blsSdk.combine_signature_shares( + signatureShares.map((s) => JSON.stringify(s)) + ); - /** - * - * Request a signed JWT from the LIT network. Before calling this function, you must know the access control conditions for the item you wish to gain authorization for. - * - * Endpoint: /web/signing/access_control_condition - * - * @param { GetSignedTokenRequest } params - * - * @returns { Promise } final JWT - * - */ - getSignedToken = async (params: GetSignedTokenRequest): Promise => { - // ========== Prepare Params ========== - const { chain, authSig, sessionSigs } = params; + log(`[signSessionKey] blsCombinedSignature:`, blsCombinedSignature); - // ========== Validation ========== - // -- validate if it's ready - if (!this.ready) { - const message = - '3 LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; - throwError({ - message, - errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, - errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, - }); - } + const publicKey = removeHexPrefix(params.pkpPublicKey); + log(`[signSessionKey] publicKey:`, publicKey); - // -- validate if this.networkPubKeySet is null - if (this.networkPubKeySet === null) { - return throwError({ - message: 'networkPubKeySet cannot be null', - errorKind: LIT_ERROR.PARAM_NULL_ERROR.kind, - errorCode: LIT_ERROR.PARAM_NULL_ERROR.name, - }); - } + const dataSigned = mostCommonString( + blsSignedData.map((s: any) => s.dataSigned) + ); + log(`[signSessionKey] dataSigned:`, dataSigned); - const paramsIsSafe = safeParams({ - functionName: 'getSignedToken', - params, + const mostCommonSiweMessage = mostCommonString( + blsSignedData.map((s: any) => s.siweMessage) + ); + + log(`[signSessionKey] mostCommonSiweMessage:`, mostCommonSiweMessage); + + const signedMessage = normalizeAndStringify(mostCommonSiweMessage); + + log(`[signSessionKey] signedMessage:`, signedMessage); + + const signSessionKeyRes: SignSessionKeyResponse = { + authSig: { + sig: JSON.stringify({ + ProofOfPossession: blsCombinedSignature, + }), + algo: 'LIT_BLS', + derivedVia: 'lit.bls', + signedMessage, + address: computeAddress(hexPrefixed(publicKey)), + }, + pkpPublicKey: publicKey, + }; + + return signSessionKeyRes; + }; + + #isSuccessNodePromises = (res: any): res is SuccessNodePromises => { + return res.success === true; + }; + + getSignSessionKeyShares = async ( + url: string, + params: GetSignSessionKeySharesProp, + requestId: string + ) => { + log('getSignSessionKeyShares'); + const urlWithPath = composeLitUrl({ + url, + endpoint: LIT_ENDPOINT.SIGN_SESSION_KEY, + }); + return await this.sendCommandToNode({ + url: urlWithPath, + data: params.body, + requestId, }); + }; - if (!paramsIsSafe) { - return throwError({ - message: `Parameter validation failed.`, - errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, - errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + /** + * Get session signatures for a set of resources + * + * High level, how this works: + * 1. Generate or retrieve session key + * 2. Generate or retrieve the wallet signature of the session key + * 3. Sign the specific resources with the session key + * + * Note: When generating session signatures for different PKPs or auth methods, + * be sure to call disconnectWeb3 to clear auth signatures stored in local storage + * + * @param { GetSessionSigsProps } params + * + * @example + * + * ```ts + * import { LitPKPResource, LitActionResource } from "@lit-protocol/auth-helpers"; +import { LitAbility } from "@lit-protocol/types"; +import { logWithRequestId } from '../../../misc/src/lib/misc'; + +const resourceAbilityRequests = [ + { + resource: new LitPKPResource("*"), + ability: LitAbility.PKPSigning, + }, + { + resource: new LitActionResource("*"), + ability: LitAbility.LitActionExecution, + }, + ]; + * ``` + */ + getSessionSigs = async ( + params: GetSessionSigsProps + ): Promise => { + // -- prepare + // Try to get it from local storage, if not generates one~ + const sessionKey = params.sessionKey ?? this.getSessionKey(); + + const sessionKeyUri = this.getSessionKeyUri(sessionKey.publicKey); + + // First get or generate the session capability object for the specified resources. + const sessionCapabilityObject = params.sessionCapabilityObject + ? params.sessionCapabilityObject + : await this.generateSessionCapabilityObjectWithWildcards( + params.resourceAbilityRequests.map((r) => r.resource) + ); + const expiration = params.expiration || LitNodeClientNodeJs.getExpiration(); + + if (!this.latestBlockhash) { + throwError({ + message: 'Eth Blockhash is undefined.', + errorKind: LIT_ERROR.INVALID_ETH_BLOCKHASH.kind, + errorCode: LIT_ERROR.INVALID_ETH_BLOCKHASH.name, }); } + const nonce = this.latestBlockhash!; - // ========== Prepare ========== - // we need to send jwt params iat (issued at) and exp (expiration) - // because the nodes may have different wall clock times - // the nodes will verify that these params are withing a grace period - const { iat, exp } = this.#getJWTParams(); + // -- (TRY) to get the wallet signature + let authSig = await this.getWalletSig({ + authNeededCallback: params.authNeededCallback, + chain: params.chain || 'ethereum', + sessionCapabilityObject, + switchChain: params.switchChain, + expiration: expiration, + sessionKey: sessionKey, + sessionKeyUri: sessionKeyUri, + nonce, - // ========== Formatting Access Control Conditions ========= - const { - error, - formattedAccessControlConditions, - formattedEVMContractConditions, - formattedSolRpcConditions, - formattedUnifiedAccessControlConditions, - }: FormattedMultipleAccs = this.getFormattedAccessControlConditions(params); + // -- for recap + resourceAbilityRequests: params.resourceAbilityRequests, - if (error) { - return throwError({ - message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, - errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, - errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, + // -- optional fields + ...(params.litActionCode && { litActionCode: params.litActionCode }), + ...(params.litActionIpfsId && { + litActionIpfsId: params.litActionIpfsId, + }), + ...(params.jsParams && { jsParams: params.jsParams }), + }); + + const needToResignSessionKey = await this.checkNeedToResignSessionKey({ + authSig, + sessionKeyUri, + resourceAbilityRequests: params.resourceAbilityRequests, + }); + + // console.log('XXX needToResignSessionKey:', needToResignSessionKey); + + // -- (CHECK) if we need to resign the session key + if (needToResignSessionKey) { + log('need to re-sign session key. Signing...'); + authSig = await this.#authCallbackAndUpdateStorageItem({ + authCallback: params.authNeededCallback, + authCallbackParams: { + chain: params.chain || 'ethereum', + statement: sessionCapabilityObject.statement, + resources: [sessionCapabilityObject.encodeAsSiweResource()], + switchChain: params.switchChain, + expiration, + sessionKey: sessionKey, + uri: sessionKeyUri, + nonce, + resourceAbilityRequests: params.resourceAbilityRequests, + + // -- optional fields + ...(params.litActionCode && { litActionCode: params.litActionCode }), + ...(params.litActionIpfsId && { + litActionIpfsId: params.litActionIpfsId, + }), + ...(params.jsParams && { jsParams: params.jsParams }), + }, }); } - // ========== Get Node Promises ========== - const wrapper = async ( - id: string - ): Promise | RejectedNodePromises> => { - const nodePromises = this._getNodePromises((url: string) => { - // -- if session key is available, use it - const authSigToSend = sessionSigs ? sessionSigs[url] : authSig; + if ( + authSig.address === '' || + authSig.derivedVia === '' || + authSig.sig === '' || + authSig.signedMessage === '' + ) { + throwError({ + message: 'No wallet signature found', + errorKind: LIT_ERROR.WALLET_SIGNATURE_NOT_FOUND_ERROR.kind, + errorCode: LIT_ERROR.WALLET_SIGNATURE_NOT_FOUND_ERROR.name, + }); + // @ts-ignore - we throw an error above, so below should never be reached + return; + } - const reqBody: SigningAccessControlConditionRequest = { - accessControlConditions: formattedAccessControlConditions, - evmContractConditions: formattedEVMContractConditions, - solRpcConditions: formattedSolRpcConditions, - unifiedAccessControlConditions: - formattedUnifiedAccessControlConditions, - chain, - authSig: authSigToSend, - iat, - exp, - }; + // ===== AFTER we have Valid Signed Session Key ===== + // - Let's sign the resources with the session key + // - 5 minutes is the default expiration for a session signature + // - Because we can generate a new session sig every time the user wants to access a resource without prompting them to sign with their wallet + const sessionExpiration = + expiration ?? new Date(Date.now() + 1000 * 60 * 5).toISOString(); - const urlWithPath = composeLitUrl({ - url, - endpoint: LIT_ENDPOINT.SIGN_ACCS, - }); + const capabilities = params.capacityDelegationAuthSig + ? [ + ...(params.capabilityAuthSigs ?? []), + params.capacityDelegationAuthSig, + authSig, + ] + : [...(params.capabilityAuthSigs ?? []), authSig]; - return this.#generatePromise(urlWithPath, reqBody, id); - }); + const signingTemplate = { + sessionKey: sessionKey.publicKey, + resourceAbilityRequests: params.resourceAbilityRequests, + capabilities, + issuedAt: new Date().toISOString(), + expiration: sessionExpiration, + }; - // -- resolve promises - const res = await this._handleNodePromises( - nodePromises, - id, - this.config.minNodeCount + const signatures: SessionSigsMap = {}; + + this.connectedNodes.forEach((nodeAddress: string) => { + const toSign: SessionSigningTemplate = { + ...signingTemplate, + nodeAddress, + }; + + const signedMessage = JSON.stringify(toSign); + + const uint8arrayKey = uint8arrayFromString( + sessionKey.secretKey, + 'base16' ); - return res; - }; - const res = await executeWithRetry< - RejectedNodePromises | SuccessNodePromises - >( - wrapper, - (error: any, requestId: string, isFinal: boolean) => { - if (!isFinal) { - logError('an error occured, attempting to retry '); + const uint8arrayMessage = uint8arrayFromString(signedMessage, 'utf8'); + const signature = nacl.sign.detached(uint8arrayMessage, uint8arrayKey); + + signatures[nodeAddress] = { + sig: uint8arrayToString(signature, 'base16'), + derivedVia: 'litSessionSignViaNacl', + signedMessage: signedMessage, + address: sessionKey.publicKey, + algo: 'ed25519', + }; + }); + + log('signatures:', signatures); + + return signatures; + }; + + /** + * Retrieves the PKP sessionSigs. + * + * @param params - The parameters for retrieving the PKP sessionSigs. + * @returns A promise that resolves to the PKP sessionSigs. + * @throws An error if any of the required parameters are missing or if `litActionCode` and `ipfsId` exist at the same time. + */ + getPkpSessionSigs = async (params: GetPkpSessionSigs) => { + const chain = params?.chain || 'ethereum'; + + const pkpSessionSigs = this.getSessionSigs({ + chain, + ...params, + authNeededCallback: async (props: AuthCallbackParams) => { + // -- validate + if (!props.expiration) { + throw new Error( + '[getPkpSessionSigs/callback] expiration is required' + ); } - }, - this.config.retryTolerance - ); - const requestId = res.requestId; - // -- case: promises rejected - if (res.success === false) { - this._throwNodeError(res as RejectedNodePromises, requestId); - } + if (!props.resources) { + throw new Error('[getPkpSessionSigs/callback]resources is required'); + } + + if (!props.resourceAbilityRequests) { + throw new Error( + '[getPkpSessionSigs/callback]resourceAbilityRequests is required' + ); + } + + // lit action code and ipfs id cannot exist at the same time + if (props.litActionCode && props.litActionIpfsId) { + throw new Error( + '[getPkpSessionSigs/callback]litActionCode and litActionIpfsId cannot exist at the same time' + ); + } + + /** + * We must provide an empty array for authMethods even if we are not using any auth methods. + * So that the nodes can serialize the request correctly. + */ + const authMethods = params.authMethods || []; + + const response = await this.signSessionKey({ + sessionKey: props.sessionKey, + statement: props.statement || 'Some custom statement.', + authMethods: [...authMethods], + pkpPublicKey: params.pkpPublicKey, + expiration: props.expiration, + resources: props.resources, + chainId: 1, + + // -- required fields + resourceAbilityRequests: props.resourceAbilityRequests, + + // -- optional fields + ...(props.litActionCode && { litActionCode: props.litActionCode }), + ...(props.litActionIpfsId && { + litActionIpfsId: props.litActionIpfsId, + }), + ...(props.jsParams && { jsParams: props.jsParams }), + }); + + return response.authSig; + }, + }); - const signatureShares: NodeBlsSigningShare[] = ( - res as SuccessNodePromises - ).values; + return pkpSessionSigs; + }; - log('signatureShares', signatureShares); + /** + * Retrieves session signatures specifically for Lit Actions. + * Unlike `getPkpSessionSigs`, this function requires either `litActionCode` or `litActionIpfsId`, and `jsParams` must be provided. + * + * @param params - The parameters required for retrieving the session signatures. + * @returns A promise that resolves with the session signatures. + */ + getLitActionSessionSigs = async (params: GetLitActionSessionSigs) => { + // Check if either litActionCode or litActionIpfsId is provided + if (!params.litActionCode && !params.litActionIpfsId) { + throw new Error( + "Either 'litActionCode' or 'litActionIpfsId' must be provided." + ); + } - // ========== Result ========== - const finalJwt: string = this.#combineSharesAndGetJWT( - signatureShares, - requestId - ); + // Check if jsParams is provided + if (!params.jsParams) { + throw new Error("'jsParams' is required."); + } - return finalJwt; + return this.getPkpSessionSigs(params); }; /** * - * Decrypt ciphertext with the LIT network. - * - * Endpoint: /web/encryption/sign + * Get Session Key URI eg. lit:session:0x1234 * + * @param publicKey is the public key of the session key + * @returns { string } the session key uri */ - decrypt = async (params: DecryptRequest): Promise => { - const { sessionSigs, chain, ciphertext, dataToEncryptHash } = params; + getSessionKeyUri = (publicKey: string): string => { + return LIT_SESSION_KEY_URI + publicKey; + }; - // ========== Validate Params ========== - // -- validate if it's ready + /** + * Authenticates an Auth Method for claiming a Programmable Key Pair (PKP). + * A {@link MintCallback} can be defined for custom on chain interactions + * by default the callback will forward to a relay server for minting on chain. + * @param {ClaimKeyRequest} params an Auth Method and {@link MintCallback} + * @returns {Promise} + */ + async claimKeyId( + params: ClaimRequest + ): Promise { if (!this.ready) { const message = - '6 LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; + 'LitNodeClient is not ready. Please call await litNodeClient.connect() first.'; throwError({ message, errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, @@ -2140,148 +2357,124 @@ const resourceAbilityRequests = [ }); } - // -- validate if this.subnetPubKey is null - if (!this.subnetPubKey) { - const message = 'subnetPubKey cannot be null'; - return throwError({ - message, + if (params.authMethod.authMethodType == AuthMethodType.WebAuthn) { + throwError({ + message: + 'Unsupported auth method type. Webauthn, and Lit Actions are not supported for claiming', errorKind: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.kind, errorCode: LIT_ERROR.LIT_NODE_CLIENT_NOT_READY_ERROR.name, }); } - - const paramsIsSafe = safeParams({ - functionName: 'decrypt', - params, - }); - - if (!paramsIsSafe) { - return throwError({ - message: `Parameter validation failed.`, - errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, - errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, - }); - } - - // ========== Hashing Access Control Conditions ========= - // hash the access control conditions - const hashOfConditions: ArrayBuffer | undefined = - await this.getHashedAccessControlConditions(params); - - if (!hashOfConditions) { - return throwError({ - message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, - errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, - errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, - }); - } - - const hashOfConditionsStr = uint8arrayToString( - new Uint8Array(hashOfConditions), - 'base16' - ); - - // ========== Formatting Access Control Conditions ========= - const { - error, - formattedAccessControlConditions, - formattedEVMContractConditions, - formattedSolRpcConditions, - formattedUnifiedAccessControlConditions, - }: FormattedMultipleAccs = this.getFormattedAccessControlConditions(params); - - if (error) { - throwError({ - message: `You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions`, - errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, - errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, - }); - } - - // ========== Assemble identity parameter ========== - const identityParam = this.#getIdentityParamForEncryption( - hashOfConditionsStr, - dataToEncryptHash - ); - - log('identityParam', identityParam); - - // ========== Get Network Signature ========== + let requestId; const wrapper = async ( id: string ): Promise | RejectedNodePromises> => { - const nodePromises = this._getNodePromises((url: string) => { - // -- if session key is available, use it - const authSigToSend = sessionSigs ? sessionSigs[url] : params.authSig; - - if (!authSigToSend) { - return throwError({ - message: `authSig is required`, - errorKind: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.kind, - errorCode: LIT_ERROR.INVALID_ARGUMENT_EXCEPTION.name, - }); + const nodePromises = this.getNodePromises((url: string) => { + if (!params.authMethod) { + throw new Error('authMethod is required'); } - const reqBody: EncryptionSignRequest = { - accessControlConditions: formattedAccessControlConditions, - evmContractConditions: formattedEVMContractConditions, - solRpcConditions: formattedSolRpcConditions, - unifiedAccessControlConditions: - formattedUnifiedAccessControlConditions, - dataToEncryptHash, - chain, - authSig: authSigToSend, - epoch: this.currentEpochNumber!, + const reqBody: JsonPKPClaimKeyRequest = { + authMethod: params.authMethod, }; - const urlWithParh = composeLitUrl({ + const urlWithPath = composeLitUrl({ url, - endpoint: LIT_ENDPOINT.ENCRYPTION_SIGN, + endpoint: LIT_ENDPOINT.PKP_CLAIM, }); - return this.#generatePromise(urlWithParh, reqBody, id); + return this.generatePromise(urlWithPath, reqBody, id); }); - // -- resolve promises - const res = await this._handleNodePromises( + const responseData = await this.handleNodePromises( nodePromises, id, - this.config.minNodeCount + this.connectedNodes.size ); - return res; + + return responseData; }; - const res = await executeWithRetry< + const responseData = await executeWithRetry< RejectedNodePromises | SuccessNodePromises >( wrapper, - (_error: string, _requestId: string, _isFinal: boolean) => { - logError('an error occured attempting to retry'); + (_error: any, _requestId: string, isFinal: boolean) => { + if (!isFinal) { + logError('an error occured, attempting to retry'); + } }, this.config.retryTolerance ); + requestId = responseData.requestId; - const requestId = res.requestId; + if (responseData.success === true) { + const nodeSignatures: Signature[] = ( + responseData as SuccessNodePromises + ).values.map((r: any) => { + const sig = ethers.utils.splitSignature(`0x${r.signature}`); + return { + r: sig.r, + s: sig.s, + v: sig.v, + }; + }); - // -- case: promises rejected - if (res.success === false) { - this._throwNodeError(res as RejectedNodePromises, requestId); - } + logWithRequestId( + requestId, + `responseData: ${JSON.stringify(responseData, null, 2)}` + ); - const signatureShares: NodeBlsSigningShare[] = ( - res as SuccessNodePromises - ).values; + const derivedKeyId = (responseData as SuccessNodePromises).values[0] + .derivedKeyId; - logWithRequestId(requestId, 'signatureShares', signatureShares); + const pubkey: string = this.computeHDPubKey(derivedKeyId); + logWithRequestId( + requestId, + `pubkey ${pubkey} derived from key id ${derivedKeyId}` + ); - // ========== Result ========== - const decryptedData = this.#decryptWithSignatureShares( - this.subnetPubKey, - uint8arrayFromString(identityParam, 'utf8'), - ciphertext, - signatureShares - ); + const relayParams: ClaimRequest<'relay'> = + params as ClaimRequest<'relay'>; - return { decryptedData }; - }; + let mintTx = ''; + if (params.mintCallback && 'signer' in params) { + mintTx = await params.mintCallback( + { + derivedKeyId, + authMethodType: params.authMethod.authMethodType, + signatures: nodeSignatures, + pubkey, + signer: (params as ClaimRequest<'client'>).signer, + ...relayParams, + }, + this.config.litNetwork as LitNetwork + ); + } else { + mintTx = await defaultMintClaimCallback( + { + derivedKeyId, + authMethodType: params.authMethod.authMethodType, + signatures: nodeSignatures, + pubkey, + ...relayParams, + }, + this.config.litNetwork as LitNetwork + ); + } + + return { + signatures: nodeSignatures, + claimedKeyId: derivedKeyId, + pubkey, + mintTx, + }; + } else { + return throwError({ + message: `Claim request has failed. Request trace id: lit_${requestId} `, + errorKind: LIT_ERROR.UNKNOWN_ERROR.kind, + errorCode: LIT_ERROR.UNKNOWN_ERROR.code, + }); + } + } } diff --git a/packages/lit-node-client/src/lib/lit-node-client.ts b/packages/lit-node-client/src/lib/lit-node-client.ts index 935594cbd8..7d84a9c697 100644 --- a/packages/lit-node-client/src/lib/lit-node-client.ts +++ b/packages/lit-node-client/src/lib/lit-node-client.ts @@ -26,7 +26,7 @@ export class LitNodeClient extends LitNodeClientNodeJs { }); // -- override configs - this.#overrideConfigsFromLocalStorage(); + this.overrideConfigsFromLocalStorage(); } /** @@ -36,7 +36,7 @@ export class LitNodeClient extends LitNodeClientNodeJs { * @returns { void } * */ - #overrideConfigsFromLocalStorage = (): void => { + overrideConfigsFromLocalStorage = (): void => { if (isNode()) return; const storageKey = 'LitNodeClientConfig'; diff --git a/packages/misc/src/lib/misc.spec.ts b/packages/misc/src/lib/misc.spec.ts index 6130923dd4..2f0e7e22d9 100644 --- a/packages/misc/src/lib/misc.spec.ts +++ b/packages/misc/src/lib/misc.spec.ts @@ -288,3 +288,12 @@ it('should not remove hex prefix if it is not present', () => { expect(result).toBe(expectedOutput); }); + +it('should get ip address', async () => { + // polyfill fetch + const fetch = require('node-fetch'); + global.fetch = fetch; + + const ipAddres = await utilsModule.getIpAddress('cayenne.litgateway.com'); + expect(ipAddres).toBe('207.244.70.36'); +}); diff --git a/packages/misc/src/lib/misc.ts b/packages/misc/src/lib/misc.ts index 1456dd4c4b..223ae1df6d 100644 --- a/packages/misc/src/lib/misc.ts +++ b/packages/misc/src/lib/misc.ts @@ -915,3 +915,26 @@ export function normalizeAndStringify(input: string): string { return normalizeAndStringify(unescaped); } } + +/** + * Retrieves the IP address associated with a given domain. + * @param domain - The domain for which to retrieve the IP address. + * @returns A Promise that resolves to the IP address. + * @throws If no IP address is found or if the domain name is invalid. + */ +export async function getIpAddress(domain: string): Promise { + const apiURL = `https://dns.google/resolve?name=${domain}&type=A`; + + try { + const response = await fetch(apiURL); + const data = await response.json(); + + if (data.Answer && data.Answer.length > 0) { + return data.Answer[0].data; + } else { + throw new Error('No IP Address found or bad domain name'); + } + } catch (error: any) { + throw new Error(error); + } +} diff --git a/packages/types/src/lib/ILitNodeClient.ts b/packages/types/src/lib/ILitNodeClient.ts index d2310b20a5..0f8b3646b0 100644 --- a/packages/types/src/lib/ILitNodeClient.ts +++ b/packages/types/src/lib/ILitNodeClient.ts @@ -6,10 +6,18 @@ import { ExecuteJsResponse, FormattedMultipleAccs, GetSignedTokenRequest, + HandshakeWithNode, + JsonExecutionRequest, JsonExecutionSdkParams, JsonHandshakeResponse, LitNodeClientConfig, MultipleAccessControlConditions, + NodeBlsSigningShare, + NodeCommandResponse, + NodeCommandServerKeysResponse, + RejectedNodePromises, + SendNodeCommand, + SuccessNodePromises, } from './interfaces'; import { ILitResource, ISessionCapabilityObject } from './models'; import { SupportedJsonRequests } from './types'; @@ -28,6 +36,35 @@ export interface ILitNodeClient { // ** IMPORTANT !! You have to create your constructor when implementing this class ** // constructor(customConfig: LitNodeClientConfig); + // ========== Scoped Class Helpers ========== + + /** + * + * Set bootstrapUrls to match the network litNetwork unless it's set to custom + * + * @returns { void } + * + */ + setCustomBootstrapUrls(): void; + + /** + * + * we need to send jwt params iat (issued at) and exp (expiration) because the nodes may have different wall clock times, the nodes will verify that these params are withing a grace period + * + */ + getJWTParams(): { iat: number; exp: number }; + + /** + * + * Combine Shares from signature shares + * + * @param { NodeBlsSigningShare } signatureShares + * + * @returns { string } final JWT (convert the sig to base64 and append to the jwt) + * + */ + combineSharesAndGetJWT(signatureShares: NodeBlsSigningShare[]): string; + /** * * Get different formats of access control conditions, eg. evm, sol, unified etc. @@ -56,6 +93,60 @@ export interface ILitNodeClient { // ========== Promise Handlers ========== + /** + * + * Get and gather node promises + * + * @param { any } callback + * + * @returns { Array> } + * + */ + getNodePromises(callback: Function): Promise[]; + + /** + * Handle node promises + * + * @param { Array> } nodePromises + * + * @param {string} requestId request Id used for logging + * @param {number} minNodeCount The minimum number of nodes we need a successful response from to continue + * @returns { Promise | RejectedNodePromises> } + * + */ + handleNodePromises( + nodePromises: Promise[], + requestId: string, + minNodeCount: number + ): Promise | RejectedNodePromises>; + + /** + * + * Throw node error + * + * @param { RejectedNodePromises } res + * + * @returns { void } + * + */ + _throwNodeError(res: RejectedNodePromises, requestId: string): void; + + // ========== Shares Resolvers ========== + + /** + * + * Get Signature + * + * @param { Array } shareData from all node promises + * + * @returns { string } signature + * + */ + getSignature(shareData: any[], requestId: string): Promise; + + // ========== API Calls to Nodes ========== + sendCommandToNode({ url, data, requestId }: SendNodeCommand): Promise; + /** * * Get JS Execution Shares from Nodes @@ -65,6 +156,21 @@ export interface ILitNodeClient { * @returns { Promise } */ + /** + * + * Handshake with SGX + * + * @param { HandshakeWithNode } params + * + * @returns { Promise } + * + */ + handshakeWithNode( + params: HandshakeWithNode, + requestId: string + ): Promise; + + // ========== Scoped Business Logics ========== /** * * Execute JS on the nodes and combine and return any resulting signatures diff --git a/packages/types/src/lib/interfaces.ts b/packages/types/src/lib/interfaces.ts index 2cb80517b2..5e3fe9120c 100644 --- a/packages/types/src/lib/interfaces.ts +++ b/packages/types/src/lib/interfaces.ts @@ -1053,6 +1053,9 @@ export interface SignSessionKeyResponse { authSig: AuthSig; } +export interface GetSignSessionKeySharesProp { + body: SessionRequestBody; +} export interface CommonGetSessionSigsProps { pkpPublicKey?: string; @@ -1174,7 +1177,17 @@ export interface LitClientSessionManager { getSessionKey: () => SessionKeyPair; isSessionKeyPair(obj: any): boolean; getExpiration: () => string; + getWalletSig: (getWalletSigProps: GetWalletSigProps) => Promise; + // #authCallbackAndUpdateStorageItem: (params: { + // authCallbackParams: AuthCallbackParams; + // authCallback?: AuthCallback; + // }) => Promise; getPkpSessionSigs: (params: GetPkpSessionSigs) => Promise; + checkNeedToResignSessionKey: (params: { + authSig: AuthSig; + sessionKeyUri: any; + resourceAbilityRequests: LitResourceAbilityRequest[]; + }) => Promise; getSessionSigs: (params: GetSessionSigsProps) => Promise; signSessionKey: ( params: SignSessionKeyProp From b595a2a4c276483022285d3d1a64fe203e657093 Mon Sep 17 00:00:00 2001 From: Josh Long Date: Mon, 3 Jun 2024 17:12:26 -0400 Subject: [PATCH 2/2] Publish 6.0.1 --- lerna.json | 2 +- packages/access-control-conditions/package.json | 4 ++-- packages/auth-browser/package.json | 4 ++-- packages/auth-helpers/package.json | 4 ++-- packages/bls-sdk/package.json | 4 ++-- packages/constants/package.json | 4 ++-- packages/constants/src/lib/version.ts | 2 +- packages/contracts-sdk/package.json | 4 ++-- packages/core/package.json | 4 ++-- packages/crypto/package.json | 4 ++-- packages/ecdsa-sdk/package.json | 4 ++-- packages/encryption/package.json | 4 ++-- packages/lit-auth-client/package.json | 4 ++-- packages/lit-node-client-nodejs/package.json | 4 ++-- packages/lit-node-client/package.json | 4 ++-- packages/logger/package.json | 4 ++-- packages/misc-browser/package.json | 4 ++-- packages/misc/package.json | 4 ++-- packages/nacl/package.json | 4 ++-- packages/pkp-base/package.json | 4 ++-- packages/pkp-client/package.json | 4 ++-- packages/pkp-cosmos/package.json | 4 ++-- packages/pkp-ethers/package.json | 4 ++-- packages/pkp-sui/package.json | 4 ++-- packages/pkp-walletconnect/package.json | 4 ++-- packages/sev-snp-utils-sdk/package.json | 4 ++-- packages/types/package.json | 4 ++-- packages/uint8arrays/package.json | 4 ++-- 28 files changed, 54 insertions(+), 54 deletions(-) diff --git a/lerna.json b/lerna.json index 8797bf38f4..a3e5866ee7 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useNx": true, "useWorkspaces": true, - "version": "6.0.0" + "version": "6.0.1" } diff --git a/packages/access-control-conditions/package.json b/packages/access-control-conditions/package.json index 1a1180ea79..db4ffdd6d0 100644 --- a/packages/access-control-conditions/package.json +++ b/packages/access-control-conditions/package.json @@ -21,7 +21,7 @@ "tags": [ "universal" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/auth-browser/package.json b/packages/auth-browser/package.json index 1f7883449b..b75fe5c51b 100644 --- a/packages/auth-browser/package.json +++ b/packages/auth-browser/package.json @@ -30,7 +30,7 @@ "tags": [ "browser" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/auth-helpers/package.json b/packages/auth-helpers/package.json index e579148933..32d5d42b1a 100644 --- a/packages/auth-helpers/package.json +++ b/packages/auth-helpers/package.json @@ -28,7 +28,7 @@ "crypto": false, "stream": false }, - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/bls-sdk/package.json b/packages/bls-sdk/package.json index d4b49caa61..661285462c 100644 --- a/packages/bls-sdk/package.json +++ b/packages/bls-sdk/package.json @@ -27,7 +27,7 @@ "buildOptions": { "genReact": false }, - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/constants/package.json b/packages/constants/package.json index ac7228ac22..ff2c2732cf 100644 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -20,7 +20,7 @@ "tags": [ "universal" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/constants/src/lib/version.ts b/packages/constants/src/lib/version.ts index 58016e0cb5..797ac2fe69 100644 --- a/packages/constants/src/lib/version.ts +++ b/packages/constants/src/lib/version.ts @@ -1 +1 @@ -export const version = '6.0.0'; +export const version = '6.0.1'; diff --git a/packages/contracts-sdk/package.json b/packages/contracts-sdk/package.json index b0dcbe9396..2df0415f84 100644 --- a/packages/contracts-sdk/package.json +++ b/packages/contracts-sdk/package.json @@ -32,7 +32,7 @@ "tags": [ "universal" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/core/package.json b/packages/core/package.json index 504dd1656b..2c54b04612 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@lit-protocol/core", - "version": "6.0.0", + "version": "6.0.0-tslib-test", "type": "commonjs", "license": "MIT", "homepage": "https://github.com/Lit-Protocol/js-sdk", @@ -27,4 +27,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/crypto/package.json b/packages/crypto/package.json index aacbef0ba4..dbf99de894 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -21,7 +21,7 @@ "tags": [ "universal" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/ecdsa-sdk/package.json b/packages/ecdsa-sdk/package.json index e1aa83561e..de355bb517 100644 --- a/packages/ecdsa-sdk/package.json +++ b/packages/ecdsa-sdk/package.json @@ -24,7 +24,7 @@ "tags": [ "universal" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 70498c0ce4..3ac2950f5e 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -25,7 +25,7 @@ "crypto": false, "stream": false }, - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/lit-auth-client/package.json b/packages/lit-auth-client/package.json index 8a73c14d78..31f07719b7 100644 --- a/packages/lit-auth-client/package.json +++ b/packages/lit-auth-client/package.json @@ -1,6 +1,6 @@ { "name": "@lit-protocol/lit-auth-client", - "version": "6.0.0", + "version": "6.0.0-tslib-test", "type": "commonjs", "license": "MIT", "homepage": "https://github.com/Lit-Protocol/js-sdk", @@ -32,4 +32,4 @@ }, "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/lit-node-client-nodejs/package.json b/packages/lit-node-client-nodejs/package.json index 3ce382c5b2..36c89e7bc6 100644 --- a/packages/lit-node-client-nodejs/package.json +++ b/packages/lit-node-client-nodejs/package.json @@ -24,7 +24,7 @@ "tags": [ "nodejs" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/lit-node-client/package.json b/packages/lit-node-client/package.json index 0d13cb5da5..893fe63071 100644 --- a/packages/lit-node-client/package.json +++ b/packages/lit-node-client/package.json @@ -28,7 +28,7 @@ "crypto": false, "stream": false }, - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/logger/package.json b/packages/logger/package.json index 0d72db7345..c483c92fc1 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@lit-protocol/logger", - "version": "6.0.0", + "version": "6.0.0-tslib-test", "type": "commonjs", "tags": [ "universal" @@ -11,4 +11,4 @@ }, "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/misc-browser/package.json b/packages/misc-browser/package.json index 2b09ea6524..db0b9e291f 100644 --- a/packages/misc-browser/package.json +++ b/packages/misc-browser/package.json @@ -21,7 +21,7 @@ "tags": [ "browser" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/misc/package.json b/packages/misc/package.json index cef1c4cba5..8e249fac6d 100644 --- a/packages/misc/package.json +++ b/packages/misc/package.json @@ -24,7 +24,7 @@ "tags": [ "universal" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/nacl/package.json b/packages/nacl/package.json index c1d5665218..9607b99035 100644 --- a/packages/nacl/package.json +++ b/packages/nacl/package.json @@ -21,7 +21,7 @@ "access": "public", "directory": "../../dist/packages/nacl" }, - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/pkp-base/package.json b/packages/pkp-base/package.json index c76f9a4af3..e26cbce507 100644 --- a/packages/pkp-base/package.json +++ b/packages/pkp-base/package.json @@ -1,6 +1,6 @@ { "name": "@lit-protocol/pkp-base", - "version": "6.0.0", + "version": "6.0.0-tslib-test", "type": "commonjs", "license": "MIT", "homepage": "https://github.com/Lit-Protocol/js-sdk", @@ -27,4 +27,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/pkp-client/package.json b/packages/pkp-client/package.json index f1f4d75bb8..d561669de9 100644 --- a/packages/pkp-client/package.json +++ b/packages/pkp-client/package.json @@ -1,6 +1,6 @@ { "name": "@lit-protocol/pkp-client", - "version": "6.0.0", + "version": "6.0.0-tslib-test", "type": "commonjs", "license": "MIT", "homepage": "https://github.com/Lit-Protocol/js-sdk", @@ -27,4 +27,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/pkp-cosmos/package.json b/packages/pkp-cosmos/package.json index ccf1395e1f..d46931622c 100644 --- a/packages/pkp-cosmos/package.json +++ b/packages/pkp-cosmos/package.json @@ -1,6 +1,6 @@ { "name": "@lit-protocol/pkp-cosmos", - "version": "6.0.0", + "version": "6.0.0-tslib-test", "type": "commonjs", "license": "MIT", "homepage": "https://github.com/Lit-Protocol/js-sdk", @@ -27,4 +27,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/pkp-ethers/package.json b/packages/pkp-ethers/package.json index 57ab7fe495..e1cc76f878 100644 --- a/packages/pkp-ethers/package.json +++ b/packages/pkp-ethers/package.json @@ -20,7 +20,7 @@ "tags": [ "universal" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/pkp-sui/package.json b/packages/pkp-sui/package.json index 7edf81531c..1c0e13fccb 100644 --- a/packages/pkp-sui/package.json +++ b/packages/pkp-sui/package.json @@ -1,6 +1,6 @@ { "name": "@lit-protocol/pkp-sui", - "version": "6.0.0", + "version": "6.0.0-tslib-test", "type": "commonjs", "license": "MIT", "homepage": "https://github.com/Lit-Protocol/js-sdk", @@ -27,4 +27,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/pkp-walletconnect/package.json b/packages/pkp-walletconnect/package.json index 02e51725b2..4dc32743a7 100644 --- a/packages/pkp-walletconnect/package.json +++ b/packages/pkp-walletconnect/package.json @@ -1,6 +1,6 @@ { "name": "@lit-protocol/pkp-walletconnect", - "version": "6.0.0", + "version": "6.0.0-tslib-test", "type": "commonjs", "license": "MIT", "homepage": "https://github.com/Lit-Protocol/js-sdk", @@ -34,4 +34,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/sev-snp-utils-sdk/package.json b/packages/sev-snp-utils-sdk/package.json index fb440cddbc..979d26eaf2 100644 --- a/packages/sev-snp-utils-sdk/package.json +++ b/packages/sev-snp-utils-sdk/package.json @@ -27,7 +27,7 @@ "buildOptions": { "genReact": false }, - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/types/package.json b/packages/types/package.json index 01041103fe..af3dcff716 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,7 +23,7 @@ "buildOptions": { "genReact": false }, - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +} diff --git a/packages/uint8arrays/package.json b/packages/uint8arrays/package.json index 0284a3b100..a0711fdd88 100644 --- a/packages/uint8arrays/package.json +++ b/packages/uint8arrays/package.json @@ -21,7 +21,7 @@ "tags": [ "universal" ], - "version": "6.0.0", + "version": "6.0.0-tslib-test", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} \ No newline at end of file +}