@settlemint/sdk-portal
Version:
Portal API client module for SettleMint SDK, providing access to smart contract portal services and APIs
305 lines (300 loc) • 10.3 kB
JavaScript
/* SettleMint Portal SDK - Blockchain Portal Optimized */
import { appendHeaders } from "@settlemint/sdk-utils/http";
import { ensureServer } from "@settlemint/sdk-utils/runtime";
import { ApplicationAccessTokenSchema, UrlOrPathSchema, validate } from "@settlemint/sdk-utils/validation";
import { initGraphQLTada, readFragment } from "gql.tada";
import { GraphQLClient } from "graphql-request";
import { z } from "zod";
import { createClient } from "graphql-ws";
import { createHash } from "node:crypto";
//#region src/utils/websocket-client.ts
/**
* Creates a GraphQL WebSocket client for the Portal API
*
* @param {WebsocketClientOptions} options - The options for the client
* @returns {Client} The GraphQL WebSocket client
* @example
* import { getWebsocketClient } from "@settlemint/sdk-portal";
*
* const client = getWebsocketClient({
* portalGraphqlEndpoint: "https://portal.settlemint.com/graphql",
* accessToken: "your-access-token",
* });
*/
function getWebsocketClient({ portalGraphqlEndpoint, accessToken }) {
if (!portalGraphqlEndpoint) {
throw new Error("portalGraphqlEndpoint is required");
}
const graphqlEndpoint = setWsProtocol(new URL(portalGraphqlEndpoint));
return createClient({ url: accessToken ? `${graphqlEndpoint.protocol}//${graphqlEndpoint.host}/${accessToken}${graphqlEndpoint.pathname}${graphqlEndpoint.search}` : graphqlEndpoint.toString() });
}
function setWsProtocol(url) {
if (url.protocol === "ws:" || url.protocol === "wss:") {
return url;
}
if (url.protocol === "http:") {
url.protocol = "ws:";
} else {
url.protocol = "wss:";
}
return url;
}
//#endregion
//#region src/utils/wait-for-transaction-receipt.ts
/**
* Waits for a blockchain transaction receipt by subscribing to transaction updates via GraphQL.
* This function polls until the transaction is confirmed or the timeout is reached.
*
* @param transactionHash - The hash of the transaction to wait for
* @param options - Configuration options for the waiting process
* @returns The transaction details including receipt information when the transaction is confirmed
* @throws Error if the transaction receipt cannot be retrieved within the specified timeout
*
* @example
* import { waitForTransactionReceipt } from "@settlemint/sdk-portal";
*
* const transaction = await waitForTransactionReceipt("0x123...", {
* portalGraphqlEndpoint: "https://example.settlemint.com/graphql",
* accessToken: "your-access-token",
* timeout: 30000 // 30 seconds timeout
* });
*/
async function waitForTransactionReceipt(transactionHash, options) {
const wsClient = getWebsocketClient(options);
const subscription = wsClient.iterate({
query: `subscription getTransaction($transactionHash: String!) {
getTransaction(transactionHash: $transactionHash) {
receipt {
transactionHash
to
status
from
type
revertReason
revertReasonDecoded
logs
events
contractAddress
}
transactionHash
from
createdAt
address
functionName
isContract
}
}`,
variables: { transactionHash }
});
const promises = [getTransactionFromSubscription(subscription)];
if (options.timeout) {
promises.push(createTimeoutPromise(options.timeout));
}
return Promise.race(promises);
}
function createTimeoutPromise(timeout) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("Transaction receipt not found")), timeout);
});
}
async function getTransactionFromSubscription(subscription) {
for await (const result of subscription) {
if (result?.data?.getTransaction?.receipt) {
return result.data.getTransaction;
}
}
throw new Error("No transaction found");
}
//#endregion
//#region src/utils/wallet-verification-challenge.ts
/**
* Custom error class for challenge-related errors
*/
var WalletVerificationChallengeError = class extends Error {
code;
constructor(message, code) {
super(message);
this.name = "ChallengeError";
this.code = code;
}
};
/**
* Hashes a pincode with a salt using SHA-256
* @param pincode - The pincode to hash
* @param salt - The salt to use in hashing
* @returns The hashed pincode as a hex string
*/
function hashPincode(pincode, salt) {
return createHash("sha256").update(`${salt}${pincode}`).digest("hex");
}
/**
* Generates a challenge response by combining a hashed pincode with a challenge
* @param pincode - The user's pincode
* @param salt - The salt provided in the challenge
* @param challenge - The challenge secret
* @returns The challenge response as a hex string
*/
function generateResponse(pincode, salt, challenge) {
const hashedPincode = hashPincode(pincode, salt);
return createHash("sha256").update(`${hashedPincode}_${challenge}`).digest("hex");
}
/**
* Handles a wallet verification challenge by generating an appropriate response
*
* @param options - The options for handling the wallet verification challenge
* @returns Promise resolving to an object containing the challenge response and optionally the verification ID
* @throws {WalletVerificationChallengeError} If the challenge cannot be created or is invalid
* @example
* import { createPortalClient } from "@settlemint/sdk-portal";
* import { handleWalletVerificationChallenge } from "@settlemint/sdk-portal";
*
* const { client, graphql } = createPortalClient({
* instance: "https://portal.example.com/graphql",
* accessToken: "your-access-token"
* });
*
* const result = await handleWalletVerificationChallenge({
* portalClient: client,
* portalGraphql: graphql,
* verificationId: "verification-123",
* userWalletAddress: "0x123...",
* code: "123456",
* verificationType: "OTP"
* });
*/
async function handleWalletVerificationChallenge({ portalClient, portalGraphql, verificationId, userWalletAddress, code, verificationType, requestId }) {
try {
const requestHeaders = new Headers();
if (requestId) {
requestHeaders.append("x-request-id", requestId);
}
const verificationChallenge = await portalClient.request(portalGraphql(`
mutation CreateWalletVerificationChallenge($userWalletAddress: String!, $verificationId: String!) {
createWalletVerificationChallenge(
userWalletAddress: $userWalletAddress
verificationId: $verificationId
) {
id
name
verificationId
verificationType
challenge {
salt
secret
}
}
}
`), {
userWalletAddress,
verificationId
}, requestHeaders);
if (!verificationChallenge.createWalletVerificationChallenge) {
throw new WalletVerificationChallengeError("No verification challenge received", "NO_CHALLENGES");
}
if (verificationType === "OTP") {
return {
challengeResponse: code.toString(),
challengeId: verificationChallenge.createWalletVerificationChallenge.id
};
}
if (verificationType === "SECRET_CODES") {
const formattedCode = code.toString().replace(/(.{5})(?=.)/, "$1-");
return {
challengeResponse: formattedCode,
challengeId: verificationChallenge.createWalletVerificationChallenge.id
};
}
const { secret, salt } = verificationChallenge.createWalletVerificationChallenge.challenge ?? {};
if (!secret || !salt) {
throw new WalletVerificationChallengeError("Invalid challenge format", "INVALID_CHALLENGE");
}
const challengeResponse = generateResponse(code.toString(), salt, secret);
return {
challengeResponse,
challengeId: verificationChallenge.createWalletVerificationChallenge.id
};
} catch (error) {
if (error instanceof WalletVerificationChallengeError) {
throw error;
}
throw new WalletVerificationChallengeError("Failed to process wallet verification challenge", "CHALLENGE_PROCESSING_ERROR");
}
}
//#endregion
//#region src/portal.ts
/**
* Schema for validating Portal client configuration options.
*/
const ClientOptionsSchema = z.object({
instance: UrlOrPathSchema,
accessToken: ApplicationAccessTokenSchema.optional(),
cache: z.enum([
"default",
"force-cache",
"no-cache",
"no-store",
"only-if-cached",
"reload"
]).optional()
});
/**
* Creates a Portal GraphQL client with the provided configuration.
*
* @param options - Configuration options for the Portal client
* @param clientOptions - Additional GraphQL client configuration options
* @returns An object containing the configured GraphQL client and graphql helper function
* @throws If the provided options fail validation
*
* @example
* import { createPortalClient } from "@settlemint/sdk-portal";
* import { loadEnv } from "@settlemint/sdk-utils/environment";
* import { createLogger, requestLogger } from "@settlemint/sdk-utils/logging";
* import type { introspection } from "@schemas/portal-env";
*
* const env = await loadEnv(false, false);
* const logger = createLogger();
*
* const { client: portalClient, graphql: portalGraphql } = createPortalClient<{
* introspection: introspection;
* disableMasking: true;
* scalars: {
* // Change unknown to the type you are using to store metadata
* JSON: unknown;
* };
* }>(
* {
* instance: env.SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT!,
* accessToken: env.SETTLEMINT_ACCESS_TOKEN!,
* },
* {
* fetch: requestLogger(logger, "portal", fetch) as typeof fetch,
* },
* );
*
* // Making GraphQL queries
* const query = portalGraphql(`
* query GetPendingTransactions {
* getPendingTransactions {
* count
* }
* }
* `);
*
* const result = await portalClient.request(query);
*/
function createPortalClient(options, clientOptions) {
ensureServer();
const validatedOptions = validate(ClientOptionsSchema, options);
const graphql = initGraphQLTada();
const fullUrl = new URL(validatedOptions.instance).toString();
return {
client: new GraphQLClient(fullUrl, {
...clientOptions,
headers: appendHeaders(clientOptions?.headers, { "x-auth-token": validatedOptions.accessToken })
}),
graphql
};
}
//#endregion
export { ClientOptionsSchema, WalletVerificationChallengeError, createPortalClient, getWebsocketClient, handleWalletVerificationChallenge, readFragment, waitForTransactionReceipt };
//# sourceMappingURL=portal.js.map