diff --git a/.changeset/poor-fans-talk.md b/.changeset/poor-fans-talk.md new file mode 100644 index 0000000000..45b8d2659a --- /dev/null +++ b/.changeset/poor-fans-talk.md @@ -0,0 +1,5 @@ +--- +'@moralisweb3/auth': minor +--- + +Added solana authentication to auth package diff --git a/packages/auth/package.json b/packages/auth/package.json index af25fd8d7f..edab608695 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -30,6 +30,7 @@ "dependencies": { "@moralisweb3/api-utils": "^2.3.1", "@moralisweb3/core": "^2.3.1", - "@moralisweb3/evm-utils": "^2.3.1" + "@moralisweb3/evm-utils": "^2.3.1", + "@moralisweb3/sol-utils": "^2.3.1" } } diff --git a/packages/auth/src/MoralisAuth.ts b/packages/auth/src/MoralisAuth.ts index d55095cbc6..d07ce362f4 100644 --- a/packages/auth/src/MoralisAuth.ts +++ b/packages/auth/src/MoralisAuth.ts @@ -1,6 +1,13 @@ import { ApiModule, MoralisCore, MoralisCoreProvider } from '@moralisweb3/core'; import { makeRequestMessage, RequestMessageOptions } from './methods/requestMessage'; -import { makeVerify, VerifyOptions } from './methods/verify'; +import { + makeVerify, + VerifyEvmData, + VerifyEvmOptions, + VerifyOptions, + VerifySolData, + VerifySolOptions, +} from './methods/verify'; export const BASE_URL = 'https://auth-api.do-prod-1.moralis.io'; @@ -24,5 +31,11 @@ export class MoralisAuth extends ApiModule { } public requestMessage = (options: RequestMessageOptions) => makeRequestMessage(this.core)(options); - public verify = (options: VerifyOptions) => makeVerify(this.core)(options); + + // Function overloading to make typescript happy + public verify(options: VerifyEvmOptions): VerifyEvmData; + public verify(options: VerifySolOptions): VerifySolData; + public verify(options: VerifyOptions) { + return makeVerify(this.core)(options); + } } diff --git a/packages/auth/src/generated/types.ts b/packages/auth/src/generated/types.ts index c1afc3dd94..6fcd922bfc 100644 --- a/packages/auth/src/generated/types.ts +++ b/packages/auth/src/generated/types.ts @@ -13,6 +13,12 @@ export interface paths { "/challenge/verify/evm": { post: operations["verifyChallengeEvm"]; }; + "/challenge/request/solana": { + post: operations["requestChallengeSolana"]; + }; + "/challenge/verify/solana": { + post: operations["verifyChallengeSolana"]; + }; } export interface components { @@ -27,8 +33,24 @@ export interface components { /** * @description EIP-155 Chain ID to which the session is bound, and the network where Contract Accounts must be resolved. * @example 1 + * @enum {string} */ - chainId: number; + chainId: + | "1" + | "3" + | "4" + | "5" + | "25" + | "42" + | "56" + | "97" + | "137" + | "250" + | "338" + | "1337" + | "43113" + | "43114" + | "80001"; /** * @description Ethereum address performing the signing conformant to capitalization encoded checksum specified in EIP-55 where applicable. * @example 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B @@ -75,8 +97,8 @@ export interface components { }; EvmChallengeResponseDto: { /** - * @description Secret Challenge ID used to identify this particular request. Is should be used at the backend of the calling service to identify the completed request. - * @example fRyt67D3eRss3RrX + * @description 17-characters Alphanumeric string Secret Challenge ID used to identify this particular request. Is should be used at the backend of the calling service to identify the completed request. + * @example fRyt67D3eRss3RrXa */ id: string; /** @@ -110,7 +132,7 @@ export interface components { }; EvmCompleteChallengeResponseDto: { /** - * @description Secret Challenge ID used to identify this particular request. Is should be used at the backend of the calling service to identify the completed request. + * @description 17-characters Alphanumeric string Secret Challenge ID used to identify this particular request. Is should be used at the backend of the calling service to identify the completed request. * @example fRyt67D3eRss3RrX */ id: string; @@ -123,8 +145,24 @@ export interface components { /** * @description EIP-155 Chain ID to which the session is bound, and the network where Contract Accounts must be resolved. * @example 1 + * @enum {string} */ - chainId: number; + chainId: + | "1" + | "3" + | "4" + | "5" + | "25" + | "42" + | "56" + | "97" + | "137" + | "250" + | "338" + | "1337" + | "43113" + | "43114" + | "80001"; /** * @description Ethereum address performing the signing conformant to capitalization encoded checksum specified in EIP-55 where applicable. * @example 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B @@ -175,6 +213,184 @@ export interface components { */ profileId: string; }; + SolanaChallengeRequestDto: { + /** + * Format: hostname + * @description RFC 4501 dns authority that is requesting the signing. + * @example defi.finance + */ + domain: string; + /** + * @description The network where Contract Accounts must be resolved. + * @example mainnet + * @enum {string} + */ + network: "mainnet" | "testnet" | "devnet"; + /** + * @description Solana public key with a length of 44 characters that is used to perform the signing + * @example 26qv4GCcx98RihuK3c4T6ozB3J7L6VwCuFVc7Ta2A3Uo + */ + address: string; + /** + * @description Human-readable ASCII assertion that the user will sign, and it must not contain ` + * `. + * @example Please confirm + */ + statement?: string; + /** + * Format: uri + * @description RFC 3986 URI referring to the resource that is the subject of the signing (as in the __subject__ of a claim). + * @example https://defi.finance/ + */ + uri: string; + /** + * Format: date-time + * @description ISO 8601 datetime string that, if present, indicates when the signed authentication message is no longer valid. + * @example 2020-01-01T00:00:00.000Z + */ + expirationTime?: string; + /** + * Format: date-time + * @description ISO 8601 datetime string that, if present, indicates when the signed authentication message will become valid. + * @example 2020-01-01T00:00:00.000Z + */ + notBefore?: string; + /** + * @description List of information or references to information the user wishes to have resolved as part of authentication by the relying party. They are expressed as RFC 3986 URIs separated by ` + * - `. + * @example [ + * "https://docs.moralis.io/" + * ] + */ + resources?: string[]; + /** + * @description Time in seconds before the challenge is expired + * @default 15 + * @example 15 + */ + timeout: number; + }; + SolanaChallengeResponseDto: { + /** + * @description 17-characters Alphanumeric string Secret Challenge ID used to identify this particular request. Is should be used at the backend of the calling service to identify the completed request. + * @example fRyt67D3eRss3RrX + */ + id: string; + /** + * @description Message that needs to be signed by the end user + * @example defi.finance wants you to sign in with your Solana account: + * 26qv4GCcx98RihuK3c4T6ozB3J7L6VwCuFVc7Ta2A3Uo + * + * I am a third party API + * + * URI: http://defi.finance + * Version: 1 + * Network: mainnet + * Nonce: PYxxb9msdjVXsMQ9x + * Issued At: 2022-08-25T11:02:34.097Z + * Expiration Time: 2022-08-25T11:12:38.243Z + * Resources: + * - https://docs.moralis.io/ + */ + message: string; + /** + * @description Unique identifier with a length of 66 characters + * @example 0xbfbcfab169c67072ff418133124480fea02175f1402aaa497daa4fd09026b0e1 + */ + profileId: string; + }; + SolanaCompleteChallengeRequestDto: { + /** + * @description Message that needs to be signed by the end user + * @example defi.finance wants you to sign in with your Solana account: + * 26qv4GCcx98RihuK3c4T6ozB3J7L6VwCuFVc7Ta2A3Uo + * + * I am a third party API + * + * URI: http://defi.finance + * Version: 1 + * Network: mainnet + * Nonce: PYxxb9msdjVXsMQ9x + * Issued At: 2022-08-25T11:02:34.097Z + * Expiration Time: 2022-08-25T11:12:38.243Z + * Resources: + * - https://docs.moralis.io/ + */ + message: string; + /** + * @description Base58 signature that needs to be used to verify end user + * @example 2pH9DqD5rve2qV4yBDshcAjWd2y8TqMx8BPb7f3KoNnuLEhE5JwjruYi4jaFaD4HN6wriLz2Vdr32kRBAJmHcyny + */ + signature: string; + }; + SolanaCompleteChallengeResponseDto: { + /** + * @description 17-characters Alphanumeric string Secret Challenge ID used to identify this particular request. Is should be used at the backend of the calling service to identify the completed request. + * @example fRyt67D3eRss3RrX + */ + id: string; + /** + * Format: hostname + * @description RFC 4501 dns authority that is requesting the signing. + * @example defi.finance + */ + domain: string; + /** + * @description The network where Contract Accounts must be resolved. + * @example mainnet + * @enum {string} + */ + network: "mainnet" | "testnet" | "devnet"; + /** + * @description Solana public key with a length of 44 characters that is used to perform the signing + * @example 26qv4GCcx98RihuK3c4T6ozB3J7L6VwCuFVc7Ta2A3Uo + */ + address: string; + /** + * @description Human-readable ASCII assertion that the user will sign, and it must not contain ` + * `. + * @example Please confirm + */ + statement?: string; + /** + * Format: uri + * @description RFC 3986 URI referring to the resource that is the subject of the signing (as in the __subject__ of a claim). + * @example https://defi.finance/ + */ + uri: string; + /** + * Format: date-time + * @description ISO 8601 datetime string that, if present, indicates when the signed authentication message is no longer valid. + * @example 2020-01-01T00:00:00.000Z + */ + expirationTime?: string; + /** + * Format: date-time + * @description ISO 8601 datetime string that, if present, indicates when the signed authentication message will become valid. + * @example 2020-01-01T00:00:00.000Z + */ + notBefore?: string; + /** + * @description List of information or references to information the user wishes to have resolved as part of authentication by the relying party. They are expressed as RFC 3986 URIs separated by ` + * - `. + * @example [ + * "https://docs.moralis.io/" + * ] + */ + resources?: string[]; + /** + * @description EIP-155 Chain ID to which the session is bound, and the network where Contract Accounts must be resolved. + * @example 1.0 + */ + version: string; + /** @example 0x1234567890abcdef0123456789abcdef1234567890abcdef */ + nonce: string; + /** + * @description Unique identifier with a length of 66 characters + * @example 0xbfbcfab169c67072ff418133124480fea02175f1402aaa497daa4fd09026b0e1 + */ + profileId: string; + }; }; } @@ -305,6 +521,38 @@ export interface operations { }; }; }; + requestChallengeSolana: { + parameters: {}; + responses: { + /** The back channel challenge containing the id to store on the api and the message to be signed by the user */ + 201: { + content: { + "application/json": components["schemas"]["SolanaChallengeResponseDto"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SolanaChallengeRequestDto"]; + }; + }; + }; + verifyChallengeSolana: { + parameters: {}; + responses: { + /** The token to be used to call the third party API from the client */ + 201: { + content: { + "application/json": components["schemas"]["SolanaCompleteChallengeResponseDto"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SolanaCompleteChallengeRequestDto"]; + }; + }; + }; } export interface external {} diff --git a/packages/auth/src/methods/requestMessage.ts b/packages/auth/src/methods/requestMessage.ts index 64098bf8d6..3869a487f3 100644 --- a/packages/auth/src/methods/requestMessage.ts +++ b/packages/auth/src/methods/requestMessage.ts @@ -1,11 +1,13 @@ +import { SolAddressish, SolNetworkish, SolAddress, SolNetwork } from '@moralisweb3/sol-utils'; import { EndpointResolver } from '@moralisweb3/api-utils'; import MoralisCore, { AuthErrorCode, MoralisAuthError } from '@moralisweb3/core'; import { EvmAddress, EvmAddressish, EvmChain, EvmChainish } from '@moralisweb3/evm-utils'; import { BASE_URL } from '../MoralisAuth'; -import { initializeChallenge } from '../resolvers/evmRequestChallenge'; +import { initializeChallengeEvm, initializeChallengeSol } from '../resolvers'; export enum AuthNetwork { EVM = 'evm', + SOLANA = 'solana', } // Imported from Swagger and adjusted for better types for Evm @@ -15,34 +17,61 @@ export interface RequestMessageEvmOptions { domain: string; chain: EvmChainish; address: EvmAddressish; - statement?: string | undefined; + statement?: string; uri: string; // TODO: allow Also Date input (and dates-string) expirationTime?: string; // TODO: allow Also Date input (and dates-string) notBefore?: string; - resources?: string[] | undefined; + resources?: string[]; timeout: number; } -export type RequestMessageOptions = RequestMessageEvmOptions; +export interface RequestMessageSolOptions { + network: 'solana'; + domain: string; + solNetwork: SolNetworkish; + address: SolAddressish; + statement?: string; + uri: string; + // TODO: allow Also Date input (and dates-string) + expirationTime?: string; + // TODO: allow Also Date input (and dates-string) + notBefore?: string; + resources?: string[]; + timeout: number; +} + +export type RequestMessageOptions = RequestMessageEvmOptions | RequestMessageSolOptions; -// eslint-disable-next-line @typescript-eslint/no-unused-vars const makeEvmRequestMessage = ( core: MoralisCore, { chain, address, network, ...options }: RequestMessageEvmOptions, ) => { - return EndpointResolver.create(core, BASE_URL, initializeChallenge).fetch({ - chainId: EvmChain.create(chain).decimal, + return EndpointResolver.create(core, BASE_URL, initializeChallengeEvm).fetch({ + chainId: EvmChain.create(chain).apiId, address: EvmAddress.create(address).checksum, ...options, }); }; +const makeSolRequestMessage = ( + core: MoralisCore, + { address, solNetwork, network, ...options }: RequestMessageSolOptions, +) => { + return EndpointResolver.create(core, BASE_URL, initializeChallengeSol).fetch({ + network: SolNetwork.create(solNetwork).network, + address: SolAddress.create(address).toString(), + ...options, + }); +}; + export const makeRequestMessage = (core: MoralisCore) => (options: RequestMessageOptions) => { switch (options.network) { - case 'evm': + case AuthNetwork.EVM: return makeEvmRequestMessage(core, options); + case AuthNetwork.SOLANA: + return makeSolRequestMessage(core, options); default: throw new MoralisAuthError({ code: AuthErrorCode.INCORRECT_NETWORK, diff --git a/packages/auth/src/methods/verify.ts b/packages/auth/src/methods/verify.ts index 4f39a0acaf..5a0618a2db 100644 --- a/packages/auth/src/methods/verify.ts +++ b/packages/auth/src/methods/verify.ts @@ -1,7 +1,8 @@ import { EndpointResolver } from '@moralisweb3/api-utils'; -import MoralisCore, { assertUnreachable } from '@moralisweb3/core'; +import MoralisCore, { AuthErrorCode, MoralisAuthError } from '@moralisweb3/core'; import { BASE_URL } from '../MoralisAuth'; -import { completeChallenge } from '../resolvers/evmVerifyChallenge'; +import { completeChallengeEvm, completeChallengeSol } from '../resolvers'; +import { AuthNetwork } from './requestMessage'; export interface VerifyEvmOptions { message: string; @@ -9,11 +10,26 @@ export interface VerifyEvmOptions { network: 'evm'; } -export type VerifyOptions = VerifyEvmOptions; +export interface VerifySolOptions { + message: string; + signature: string; + network: 'solana'; +} + +export type VerifyOptions = VerifyEvmOptions | VerifySolOptions; + +export type VerifyEvmData = ReturnType; +export type VerifySolData = ReturnType; -// eslint-disable-next-line @typescript-eslint/no-unused-vars const makeEvmVerify = (core: MoralisCore, { network, ...options }: VerifyEvmOptions) => { - return EndpointResolver.create(core, BASE_URL, completeChallenge).fetch({ + return EndpointResolver.create(core, BASE_URL, completeChallengeEvm).fetch({ + message: options.message, + signature: options.signature, + }); +}; + +const makeSolVerify = (core: MoralisCore, { network, ...options }: VerifySolOptions) => { + return EndpointResolver.create(core, BASE_URL, completeChallengeSol).fetch({ message: options.message, signature: options.signature, }); @@ -21,9 +37,16 @@ const makeEvmVerify = (core: MoralisCore, { network, ...options }: VerifyEvmOpti export const makeVerify = (core: MoralisCore) => (options: VerifyOptions) => { switch (options.network) { - case 'evm': + case AuthNetwork.EVM: return makeEvmVerify(core, options); + case AuthNetwork.SOLANA: + return makeSolVerify(core, options); default: - return assertUnreachable(options.network); + throw new MoralisAuthError({ + code: AuthErrorCode.INCORRECT_NETWORK, + message: `Incorrect network provided. Got "${options.network}", Valid values are: ${Object.values(AuthNetwork) + .map((value) => `"${value}"`) + .join(', ')}`, + }); } }; diff --git a/packages/auth/src/resolvers/evmRequestChallenge.ts b/packages/auth/src/resolvers/evmRequestChallenge.ts index 60785c67cd..f11f456534 100644 --- a/packages/auth/src/resolvers/evmRequestChallenge.ts +++ b/packages/auth/src/resolvers/evmRequestChallenge.ts @@ -31,7 +31,7 @@ const apiToResult = (apiData: ApiResult) => { }; }; -export const initializeChallenge = createEndpointFactory(() => +export const initializeChallengeEvm = createEndpointFactory(() => createEndpoint({ name, getUrl: () => `/challenge/request/evm`, diff --git a/packages/auth/src/resolvers/evmVerifyChallenge.ts b/packages/auth/src/resolvers/evmVerifyChallenge.ts index 158408979f..8b073a0902 100644 --- a/packages/auth/src/resolvers/evmVerifyChallenge.ts +++ b/packages/auth/src/resolvers/evmVerifyChallenge.ts @@ -14,7 +14,7 @@ const bodyParams = ['message', 'signature'] as const; type ApiResult = operations[Name]['responses']['201']['content']['application/json']; -export const completeChallenge = createEndpointFactory(() => +export const completeChallengeEvm = createEndpointFactory(() => createEndpoint({ name: 'Verify Challenge (EVM)', getUrl: () => `/challenge/verify/evm`, diff --git a/packages/auth/src/resolvers/index.ts b/packages/auth/src/resolvers/index.ts new file mode 100644 index 0000000000..970478d373 --- /dev/null +++ b/packages/auth/src/resolvers/index.ts @@ -0,0 +1,4 @@ +export * from './evmRequestChallenge'; +export * from './evmVerifyChallenge'; +export * from './solRequestChallenge'; +export * from './solVerifyChallenge'; diff --git a/packages/auth/src/resolvers/solRequestChallenge.ts b/packages/auth/src/resolvers/solRequestChallenge.ts new file mode 100644 index 0000000000..04ebb764a1 --- /dev/null +++ b/packages/auth/src/resolvers/solRequestChallenge.ts @@ -0,0 +1,46 @@ +import { createEndpoint, createEndpointFactory } from '@moralisweb3/api-utils'; +import { toCamelCase } from '@moralisweb3/core'; +import { operations } from '../generated/types'; + +const name = 'requestChallengeSolana'; + +type Name = typeof name; +type BodyParams = operations[Name]['requestBody']['content']['application/json']; +type ApiParams = BodyParams; +const method = 'post'; +const bodyParams = [ + 'domain', + 'network', + 'address', + 'statement', + 'uri', + 'expirationTime', + 'notBefore', + 'resources', + 'timeout', +] as const; +type Params = ApiParams; + +type ApiResult = operations[Name]['responses']['201']['content']['application/json']; + +const apiToResult = (apiData: ApiResult) => { + const data = toCamelCase(apiData); + + return { + ...data, + }; +}; + +export const initializeChallengeSol = createEndpointFactory(() => + createEndpoint({ + name, + getUrl: () => `/challenge/request/solana`, + apiToResult, + resultToJson: (data) => ({ + ...data, + }), + parseParams: (params: Params): ApiParams => params, + method, + bodyParams, + }), +); diff --git a/packages/auth/src/resolvers/solVerifyChallenge.ts b/packages/auth/src/resolvers/solVerifyChallenge.ts new file mode 100644 index 0000000000..86e065b791 --- /dev/null +++ b/packages/auth/src/resolvers/solVerifyChallenge.ts @@ -0,0 +1,36 @@ +import { SolNetwork, SolAddress } from '@moralisweb3/sol-utils'; +import { maybe, toCamelCase } from '@moralisweb3/core'; +import { createEndpoint, createEndpointFactory } from '@moralisweb3/api-utils'; +import { operations } from '../generated/types'; + +const name = 'verifyChallengeSolana'; + +type Name = typeof name; +type BodyParams = operations[Name]['requestBody']['content']['application/json']; +type ApiParams = BodyParams; +type Params = ApiParams; +const method = 'post'; +const bodyParams = ['message', 'signature'] as const; + +type ApiResult = operations[Name]['responses']['201']['content']['application/json']; + +export const completeChallengeSol = createEndpointFactory(() => + createEndpoint({ + name: 'Verify Challenge (Solana)', + getUrl: () => `/challenge/verify/solana`, + apiToResult: ({ network, ...data }: ApiResult) => ({ + ...data, + solNetwork: SolNetwork.create(network), + address: SolAddress.create(data.address), + expirationTime: maybe(data.expirationTime, (value) => new Date(value)), + }), + resultToJson: (result) => ({ + ...toCamelCase(result), + solNetwork: result.solNetwork.format(), + address: result.address.format(), + }), + parseParams: (params: Params): ApiParams => params, + method, + bodyParams, + }), +); diff --git a/packages/evmUtils/src/dataTypes/EvmChain/EvmChain.ts b/packages/evmUtils/src/dataTypes/EvmChain/EvmChain.ts index a09e6ce13c..3945701d9e 100644 --- a/packages/evmUtils/src/dataTypes/EvmChain/EvmChain.ts +++ b/packages/evmUtils/src/dataTypes/EvmChain/EvmChain.ts @@ -324,6 +324,30 @@ export class EvmChain implements MoralisData, EvmChainable { | '0x19'; } + /** + * Validate and cast to api compatible id + * + * @example chain.apiId // 1 + */ + get apiId() { + return this._value as + | '1' + | '3' + | '4' + | '5' + | '25' + | '42' + | '56' + | '97' + | '137' + | '250' + | '338' + | '1337' + | '43113' + | '43114' + | '80001'; + } + /** * Returns the name of the chain * @example chain.name // "Ethereum"