From bdc587b20752ade96edeb944ae22a109c114fa5c Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 18:24:54 +0300 Subject: [PATCH 01/16] feat: update constructor credentials and networks --- src/clients/indexerClient.ts | 18 +++-- src/clients/walletClient.ts | 56 +++++---------- src/index.ts | 7 ++ src/networks.ts | 118 +++++++++++++++++++++++++++++++ src/omsClient.ts | 11 +-- src/omsEnvironment.ts | 3 +- src/signedFetch.ts | 15 ++-- src/types/evmTypes.ts | 4 +- src/types/transactionTypes.ts | 4 +- src/utils/constants.ts | 1 - src/utils/networkBindings.ts | 31 -------- tests/credentialSigner.test.ts | 11 +-- tests/indexerClient.test.ts | 4 +- tests/networks.test.ts | 53 ++++++++++++++ tests/oidcRedirectAuth.test.ts | 14 ++-- tests/walletAccess.test.ts | 3 +- tests/walletErrors.test.ts | 9 ++- tests/walletSession.test.ts | 30 +++++--- tests/walletSigning.test.ts | 14 ++-- tests/walletTransactions.test.ts | 13 ++-- type-tests/oidcProviderTypes.ts | 49 +++++++++++-- 21 files changed, 325 insertions(+), 143 deletions(-) create mode 100644 src/networks.ts delete mode 100644 src/utils/networkBindings.ts create mode 100644 tests/networks.test.ts diff --git a/src/clients/indexerClient.ts b/src/clients/indexerClient.ts index ba50e2e..6c50eaa 100644 --- a/src/clients/indexerClient.ts +++ b/src/clients/indexerClient.ts @@ -1,7 +1,7 @@ // Converted from Swift IndexerClient. import {HttpClient} from "../httpClient.js"; -import {NetworkBindings} from "../utils/networkBindings.js"; +import {findNetworkById, findNetworkByName} from "../networks.js"; import {errorMessage, OmsRequestError, OmsResponseError} from "../errors.js"; export interface TokenBalancesPage { @@ -74,19 +74,17 @@ export interface OmsEnvironment { } export class IndexerClient { - private readonly projectAccessKey: string; + private readonly publicApiKey: string; private readonly environment: OmsEnvironment; private readonly client: HttpClient; - private readonly networks: NetworkBindings; constructor(params: { - projectAccessKey: string, + publicApiKey: string, environment: OmsEnvironment }) { - this.projectAccessKey = params.projectAccessKey; + this.publicApiKey = params.publicApiKey; this.environment = params.environment; this.client = new HttpClient(); - this.networks = new NetworkBindings(); } async getTokenBalances(params: { @@ -194,16 +192,16 @@ export class IndexerClient { } private indexerNetworkValue(chainId: string): string { - const normalized = chainId.toLowerCase(); + const normalized = chainId.trim().toLowerCase(); if (/^\d+$/.test(normalized)) { - return this.networks.findChainNameById(BigInt(normalized)) ?? normalized; + return findNetworkById(Number(normalized))?.name ?? normalized; } - return normalized; + return findNetworkByName(normalized)?.name ?? normalized; } private defaultHeaders(): Record { return { - "X-Access-Key": this.projectAccessKey, + "X-Access-Key": this.publicApiKey, Accept: "application/json", }; } diff --git a/src/clients/walletClient.ts b/src/clients/walletClient.ts index b8ee38d..45424fb 100644 --- a/src/clients/walletClient.ts +++ b/src/clients/walletClient.ts @@ -54,8 +54,7 @@ import { Fetch, CredentialInfo, } from '../generated/waas.gen.js' -import {NetworkBindings} from "../utils/networkBindings.js"; -import {Network} from "../types/evmTypes.js"; +import type {Network} from "../networks.js"; import { FeeOptionSelector, FeeOptionWithBalance, @@ -195,7 +194,7 @@ interface PendingOidcRedirectAuth { walletType: WalletType; redirectUri: string; issuer: string; - waasAuthScope: string; + projectId: string; } interface ResolvedOidcProvider { @@ -214,11 +213,10 @@ export class WalletClient { private readonly publicClient: WalletPublicclient private readonly storage: StorageManager private readonly redirectAuthStorage?: StorageManager - private readonly networks: NetworkBindings private readonly credentialSigner: CredentialSigner private readonly indexerClient: IndexerClient private readonly environment: Env - private readonly waasAuthScope: string + private readonly projectId: string private readonly fastTransactionStatusPollIntervalMs = 400 private readonly fastTransactionStatusPollCount = 5 private readonly transactionStatusPollIntervalMs = 2_000 @@ -236,7 +234,8 @@ export class WalletClient { private challenge = '' constructor(params: { - projectAccessKey: string, + publicApiKey: string, + projectId: string, environment: Env, storage?: StorageManager redirectAuthStorage?: StorageManager @@ -246,7 +245,7 @@ export class WalletClient { this.storage = params.storage ?? createDefaultStorage() this.redirectAuthStorage = params.redirectAuthStorage ?? defaultRedirectAuthStorage() this.credentialSigner = params.credentialSigner ?? new WebCryptoP256CredentialSigner() - this.waasAuthScope = params.environment.auth?.waasAuthScope ?? Constants.defaultWaasAuthScope + this.projectId = params.projectId const storedId = this.storage.get(Constants.walletIdStorageKey) const storedAddress = this.storage.get(Constants.walletAddressStorageKey) @@ -265,17 +264,16 @@ export class WalletClient { this.sessionEmail = undefined } - const signedFetch = createSignedFetch(params.projectAccessKey, this.credentialSigner, this.waasAuthScope) + const signedFetch = createSignedFetch(params.publicApiKey, this.credentialSigner, this.projectId) this.client = new Walletclient(params.environment.walletApiUrl, signedFetch) this.publicClient = new WalletPublicclient( params.environment.walletApiUrl, - createAccessKeyFetch(params.projectAccessKey), + createAccessKeyFetch(params.publicApiKey), ) this.indexerClient = new IndexerClient({ - projectAccessKey: params.projectAccessKey, + publicApiKey: params.publicApiKey, environment: params.environment, }) - this.networks = new NetworkBindings() } /** Durable metadata for the completed wallet session. */ @@ -374,7 +372,7 @@ export class WalletClient { const nonce = generateOidcNonce() const state = encodeOidcState({ nonce, - scope: this.waasAuthScope, + scope: this.projectId, ...(oauthRedirectUri !== params.redirectUri ? {redirect_uri: params.redirectUri} : {}), }) @@ -385,7 +383,7 @@ export class WalletClient { walletType: params.walletType ?? WalletType.Ethereum, redirectUri: params.redirectUri, issuer: provider.config.issuer, - waasAuthScope: this.waasAuthScope, + projectId: this.projectId, }) const authorizeParams = { @@ -941,7 +939,7 @@ export class WalletClient { typeof parsed.nonce !== 'string' || typeof parsed.redirectUri !== 'string' || typeof parsed.issuer !== 'string' || - typeof parsed.waasAuthScope !== 'string' + typeof parsed.projectId !== 'string' ) { throw new Error('Pending OIDC redirect auth is invalid') } @@ -953,7 +951,7 @@ export class WalletClient { walletType: isWalletType(parsed.walletType) ? parsed.walletType : WalletType.Ethereum, redirectUri: parsed.redirectUri, issuer: parsed.issuer, - waasAuthScope: parsed.waasAuthScope, + projectId: parsed.projectId, } } catch (error) { throw error instanceof Error ? error : new Error('Pending OIDC redirect auth is invalid') @@ -965,7 +963,7 @@ export class WalletClient { if (state.nonce !== pending.nonce) { throw new Error('OIDC state nonce mismatch') } - if (state.scope !== pending.waasAuthScope) { + if (state.scope !== pending.projectId) { throw new Error('OIDC state scope mismatch') } if (state.redirect_uri !== undefined && state.redirect_uri !== pending.redirectUri) { @@ -1212,29 +1210,11 @@ export class WalletClient { } private parseWalletNetwork(network: Network): string { - if (typeof network === 'string') { - const normalized = network.toLowerCase() - return this.networks.getChainIdByName(normalized)?.toString() ?? normalized - } else if (typeof network === 'bigint') { - return network.toString() - } else { - return BigInt(network.id).toString() - } + return network.id.toString() } private parseIndexerNetwork(network: Network): string { - if (typeof network === 'string') { - const normalized = network.toLowerCase() - if (/^\d+$/.test(normalized)) { - return this.networks.findChainNameById(BigInt(normalized)) ?? normalized - } - return normalized - } else if (typeof network === 'bigint') { - return this.networks.findChainNameById(network) ?? network.toString() - } else { - const chainId = BigInt(network.id) - return this.networks.findChainNameById(chainId) ?? chainId.toString() - } + return network.name } private isNativeToken(feeOption: FeeOption): boolean { @@ -1285,12 +1265,12 @@ function defaultRedirectAuthStorage(): StorageManager | undefined { : undefined } -function createAccessKeyFetch(projectAccessKey: string): Fetch { +function createAccessKeyFetch(publicApiKey: string): Fetch { return async (input: RequestInfo, init?: RequestInit): Promise => { const existingHeaders = (init?.headers ?? {}) as Record const headers: Record = { ...existingHeaders, - 'X-Access-Key': projectAccessKey, + 'X-Access-Key': publicApiKey, } return globalThis.fetch(input, {...init, headers}) diff --git a/src/index.ts b/src/index.ts index ef35867..34f7fe9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,13 @@ export { createDefaultStorage, type StorageManager, } from './storageManager.js' +export { + Networks, + findNetworkById, + findNetworkByName, + supportedNetworks, + type Network, +} from './networks.js' export { TransactionMode, TransactionStatus, diff --git a/src/networks.ts b/src/networks.ts new file mode 100644 index 0000000..2471957 --- /dev/null +++ b/src/networks.ts @@ -0,0 +1,118 @@ +export interface Network { + readonly id: number + readonly name: string + readonly nativeTokenSymbol: string + readonly explorerUrl: string +} + +export const Networks = { + mainnet: { + id: 1, + name: 'mainnet', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://etherscan.io', + }, + sepolia: { + id: 11155111, + name: 'sepolia', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://sepolia.etherscan.io', + }, + polygon: { + id: 137, + name: 'polygon', + nativeTokenSymbol: 'POL', + explorerUrl: 'https://polygonscan.com', + }, + amoy: { + id: 80002, + name: 'amoy', + nativeTokenSymbol: 'POL', + explorerUrl: 'https://amoy.polygonscan.com', + }, + arbitrum: { + id: 42161, + name: 'arbitrum', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://arbiscan.io', + }, + arbitrumSepolia: { + id: 421614, + name: 'arbitrum-sepolia', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://sepolia.arbiscan.io', + }, + optimism: { + id: 10, + name: 'optimism', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://optimistic.etherscan.io', + }, + optimismSepolia: { + id: 11155420, + name: 'optimism-sepolia', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://sepolia-optimism.etherscan.io', + }, + base: { + id: 8453, + name: 'base', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://basescan.org', + }, + baseSepolia: { + id: 84532, + name: 'base-sepolia', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://sepolia.basescan.org', + }, + bsc: { + id: 56, + name: 'bsc', + nativeTokenSymbol: 'BNB', + explorerUrl: 'https://bscscan.com', + }, + bscTestnet: { + id: 97, + name: 'bsc-testnet', + nativeTokenSymbol: 'BNB', + explorerUrl: 'https://testnet.bscscan.com', + }, + arbitrumNova: { + id: 42170, + name: 'arbitrum-nova', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://nova.arbiscan.io', + }, + avalanche: { + id: 43114, + name: 'avalanche', + nativeTokenSymbol: 'AVAX', + explorerUrl: 'https://subnets.avax.network/c-chain', + }, + avalancheTestnet: { + id: 43113, + name: 'avalanche-testnet', + nativeTokenSymbol: 'AVAX', + explorerUrl: 'https://subnets-test.avax.network/c-chain', + }, + katana: { + id: 747474, + name: 'katana', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://katanascan.com', + }, +} as const satisfies Record; + +export const supportedNetworks: readonly Network[] = Object.freeze(Object.values(Networks)); + +const networksById = new Map(supportedNetworks.map(network => [network.id, network])); +const networksByName = new Map(supportedNetworks.map(network => [network.name.toLowerCase(), network])); + +export function findNetworkById(chainId: number): Network | undefined { + return networksById.get(chainId); +} + +export function findNetworkByName(name: string): Network | undefined { + return networksByName.get(name.trim().toLowerCase()); +} diff --git a/src/omsClient.ts b/src/omsClient.ts index 232169e..8e59f12 100644 --- a/src/omsClient.ts +++ b/src/omsClient.ts @@ -3,9 +3,11 @@ import {defaultOmsEnvironment, OmsEnvironment} from "./omsEnvironment.js"; import {createDefaultStorage, StorageManager} from "./storageManager.js"; import {IndexerClient} from "./clients/indexerClient.js"; import type {CredentialSigner} from "./credentialSigner.js"; +import {supportedNetworks} from "./networks.js"; interface OMSClientBaseParams { - projectAccessKey: string; + publicApiKey: string; + projectId: string; storage?: StorageManager; redirectAuthStorage?: StorageManager; credentialSigner?: CredentialSigner; @@ -19,13 +21,15 @@ type DefaultOMSClientParams = OMSClientBaseParams & {environment?: undefined}; class OMSClientImpl { public readonly wallet: WalletClient; public readonly indexer: IndexerClient; + public readonly supportedNetworks = supportedNetworks; constructor(params: OMSClientBaseParams & {environment?: Env}) { const environment = (params.environment ?? defaultOmsEnvironment) as Env; const storage = params.storage ?? createDefaultStorage() this.wallet = new WalletClient({ - projectAccessKey: params.projectAccessKey, + publicApiKey: params.publicApiKey, + projectId: params.projectId, environment, storage, redirectAuthStorage: params.redirectAuthStorage, @@ -33,11 +37,10 @@ class OMSClientImpl { }); this.indexer = new IndexerClient({ - projectAccessKey: params.projectAccessKey, + publicApiKey: params.publicApiKey, environment }); } - } export type OMSClient = OMSClientImpl; diff --git a/src/omsEnvironment.ts b/src/omsEnvironment.ts index 907e38a..85eaff2 100644 --- a/src/omsEnvironment.ts +++ b/src/omsEnvironment.ts @@ -12,7 +12,6 @@ export interface OidcProviderConfig { export interface OmsAuthConfig< OidcProviders extends Record = Record, > { - waasAuthScope?: string; oidcProviders?: OidcProviders; } @@ -25,7 +24,7 @@ export interface OmsEnvironment< } export const defaultOmsEnvironment = { - walletApiUrl: "https://d1sctl7y41hot5.cloudfront.net", + walletApiUrl: "https://d26giflyqapd29.cloudfront.net", indexerUrlTemplate: "https://dev-{value}-indexer.sequence.app/rpc/Indexer/", auth: { oidcProviders: { diff --git a/src/signedFetch.ts b/src/signedFetch.ts index f5cad53..b901741 100644 --- a/src/signedFetch.ts +++ b/src/signedFetch.ts @@ -1,5 +1,4 @@ import {Fetch} from "./generated/waas.gen.js"; -import {Constants} from "./utils/constants.js"; import {RequestUtils} from "./utils/requestUtils.js"; import type {CredentialSigner} from "./credentialSigner.js"; @@ -7,19 +6,19 @@ async function buildWalletSignatureHeader( endpoint: string, signer: CredentialSigner, payload: string, - waasAuthScope: string, + projectId: string, ): Promise { const credentialId = await signer.credentialId() const nonce = await signer.nextNonce() - const preimage = RequestUtils.buildWalletRequestPreimage(endpoint, nonce, waasAuthScope, payload) + const preimage = RequestUtils.buildWalletRequestPreimage(endpoint, nonce, projectId, payload) const signature = await signer.sign(preimage) - return RequestUtils.buildWalletSignatureHeader(signer.signingAlgorithm, waasAuthScope, credentialId, nonce, signature) + return RequestUtils.buildWalletSignatureHeader(signer.signingAlgorithm, projectId, credentialId, nonce, signature) } export function createSignedFetch( - projectAccessKey: string, + publicApiKey: string, signer: CredentialSigner, - waasAuthScope: string = Constants.defaultWaasAuthScope, + projectId: string, ): Fetch { return async (input: RequestInfo, init?: RequestInit): Promise => { const url = typeof input === 'string' ? input : (input as Request).url @@ -28,12 +27,12 @@ export function createSignedFetch( const body = typeof init?.body === 'string' ? init.body : '' - const signatureHeader = await buildWalletSignatureHeader(endpoint, signer, body, waasAuthScope) + const signatureHeader = await buildWalletSignatureHeader(endpoint, signer, body, projectId) const existingHeaders = (init?.headers ?? {}) as Record const headers: Record = { ...existingHeaders, - 'X-Access-Key': projectAccessKey, + 'X-Access-Key': publicApiKey, 'OMS-Wallet-Signature': signatureHeader, } diff --git a/src/types/evmTypes.ts b/src/types/evmTypes.ts index 5dfa310..5ec00e1 100644 --- a/src/types/evmTypes.ts +++ b/src/types/evmTypes.ts @@ -1,3 +1 @@ -import {Chain} from "viem"; - -export type Network = string | bigint | Chain; \ No newline at end of file +export type {Network} from "../networks.js"; diff --git a/src/types/transactionTypes.ts b/src/types/transactionTypes.ts index 71a41a8..9ee1db5 100644 --- a/src/types/transactionTypes.ts +++ b/src/types/transactionTypes.ts @@ -1,5 +1,5 @@ -import {Abi, Address, Chain, ContractFunctionName, EncodeFunctionDataParameters, Hex} from "viem"; -import {Network} from "./evmTypes.js"; +import {Abi, Address, ContractFunctionName, EncodeFunctionDataParameters, Hex} from "viem"; +import type {Network} from "../networks.js"; import type { FeeOption, FeeOptionSelection, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d0c116a..4ac9747 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,6 +4,5 @@ export const Constants = { sessionExpiresAtStorageKey: 'omsWallet_session_expires_at', sessionLoginTypeStorageKey: 'omsWallet_session_login_type', sessionEmailStorageKey: 'omsWallet_session_email', - defaultWaasAuthScope: 'proj_1', redirectAuthStorageKey: 'omsWallet_oidc_redirect_auth', } as const diff --git a/src/utils/networkBindings.ts b/src/utils/networkBindings.ts deleted file mode 100644 index 2d5e478..0000000 --- a/src/utils/networkBindings.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface Network { - id: bigint - name: string -} - -export class NetworkBindings { - private readonly byId: ReadonlyMap - private readonly byName: ReadonlyMap - - constructor(networks: readonly Network[] = NetworkBindings.DEFAULT_NETWORKS) { - this.byId = new Map(networks.map(n => [n.id, n.name])) - this.byName = new Map(networks.map(n => [n.name.toLowerCase(), n.id])) - } - - findChainNameById(id: bigint): string | undefined { - return this.byId.get(id) - } - - getChainNameById(id: bigint): string { - return this.findChainNameById(id) ?? 'undefined' - } - - getChainIdByName(name: string): bigint | undefined { - return this.byName.get(name.toLowerCase()) - } - - static readonly DEFAULT_NETWORKS: readonly Network[] = [ - { id: BigInt(137), name: 'polygon' }, - { id: BigInt(80002), name: 'amoy' }, - ] -} diff --git a/tests/credentialSigner.test.ts b/tests/credentialSigner.test.ts index 77764f2..cc30f54 100644 --- a/tests/credentialSigner.test.ts +++ b/tests/credentialSigner.test.ts @@ -1,30 +1,31 @@ import {describe, expect, it} from "vitest"; -import {Constants} from "../src/utils/constants"; import {RequestUtils} from "../src/utils/requestUtils"; describe("RequestUtils", () => { it("builds wallet auth request vectors", () => { + const projectId = "project-id"; + expect(RequestUtils.buildWalletRequestPreimage( "/CommitVerifier", "42", - Constants.defaultWaasAuthScope, + projectId, "{\"walletId\":\"wallet-id\"}", )).toBe( "POST /rpc/Wallet/CommitVerifier\n" + "nonce: 42\n" + - `scope: ${Constants.defaultWaasAuthScope}\n\n` + + `scope: ${projectId}\n\n` + "{\"walletId\":\"wallet-id\"}", ); expect(RequestUtils.buildWalletSignatureHeader( "ecdsa-p256-sha256", - Constants.defaultWaasAuthScope, + projectId, `0x04${"11".repeat(64)}`, "42", `0x${"22".repeat(64)}`, )).toBe( - `alg="ecdsa-p256-sha256", scope="${Constants.defaultWaasAuthScope}", cred="0x04${"11".repeat(64)}", nonce=42, sig="0x${"22".repeat(64)}"`, + `alg="ecdsa-p256-sha256", scope="${projectId}", cred="0x04${"11".repeat(64)}", nonce=42, sig="0x${"22".repeat(64)}"`, ); }); }); diff --git a/tests/indexerClient.test.ts b/tests/indexerClient.test.ts index 4526d0f..f6749a2 100644 --- a/tests/indexerClient.test.ts +++ b/tests/indexerClient.test.ts @@ -12,7 +12,7 @@ describe("IndexerClient errors", () => { vi.stubGlobal("fetch", vi.fn(async () => new Response("not-json", {status: 200}))); const indexer = new IndexerClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", environment: testEnvironment(), }); @@ -32,7 +32,7 @@ describe("IndexerClient errors", () => { vi.stubGlobal("fetch", vi.fn(async () => new Response("Bad Gateway", {status: 502}))); const indexer = new IndexerClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", environment: testEnvironment(), }); diff --git a/tests/networks.test.ts b/tests/networks.test.ts new file mode 100644 index 0000000..1fae63c --- /dev/null +++ b/tests/networks.test.ts @@ -0,0 +1,53 @@ +import {describe, expect, it} from "vitest"; + +import { + Networks, + OMSClient, + findNetworkById, + findNetworkByName, + supportedNetworks, +} from "../src"; + +describe("Networks", () => { + it("exposes the supported network registry", () => { + expect(supportedNetworks).toEqual([ + Networks.mainnet, + Networks.sepolia, + Networks.polygon, + Networks.amoy, + Networks.arbitrum, + Networks.arbitrumSepolia, + Networks.optimism, + Networks.optimismSepolia, + Networks.base, + Networks.baseSepolia, + Networks.bsc, + Networks.bscTestnet, + Networks.arbitrumNova, + Networks.avalanche, + Networks.avalancheTestnet, + Networks.katana, + ]); + expect(Networks.katana).toEqual({ + id: 747474, + name: "katana", + nativeTokenSymbol: "ETH", + explorerUrl: "https://katanascan.com", + }); + }); + + it("looks up networks by id or name", () => { + expect(findNetworkById(43113)).toBe(Networks.avalancheTestnet); + expect(findNetworkById(421614)).toBe(Networks.arbitrumSepolia); + expect(findNetworkByName("base-sepolia")).toBe(Networks.baseSepolia); + }); + + it("is available from OMSClient", () => { + const oms = new OMSClient({ + publicApiKey: "public-api-key", + projectId: "project-id", + }); + + expect(oms.supportedNetworks).toBe(supportedNetworks); + }); +}); diff --git a/tests/oidcRedirectAuth.test.ts b/tests/oidcRedirectAuth.test.ts index 6035297..3c0a31c 100644 --- a/tests/oidcRedirectAuth.test.ts +++ b/tests/oidcRedirectAuth.test.ts @@ -81,7 +81,7 @@ describe("WalletClient OIDC redirect auth", () => { expect(authorizeUrl.searchParams.get("login_hint")).toBe("user@example.com"); const state = decodeOidcState(result.state); - expect(state.scope).toBe(Constants.defaultWaasAuthScope); + expect(state.scope).toBe("project-id"); expect(state.redirect_uri).toBe("https://app.example/auth/callback"); expect(redirectAuthStorage.get(Constants.redirectAuthStorageKey)).toContain("verifier-1"); }); @@ -114,7 +114,7 @@ describe("WalletClient OIDC redirect auth", () => { expect(state.redirect_uri).toBe("http://localhost:5173/auth/callback"); }); - it("uses provider relay defaults and custom WaaS auth scope in headers and state", async () => { + it("uses provider relay defaults and project ID in headers and state", async () => { const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { const headers = init?.headers as Record; expect(headers["OMS-Wallet-Signature"]).toContain('scope="proj_custom"'); @@ -132,11 +132,11 @@ describe("WalletClient OIDC redirect auth", () => { const wallet = createWalletClient({ redirectAuthStorage: new MemoryStorageManager(), credentialSigner: signer, + projectId: "proj_custom", environment: defineOmsEnvironment({ walletApiUrl: "https://wallet.example", indexerUrlTemplate: "https://indexer.example/{value}", auth: { - waasAuthScope: "proj_custom", oidcProviders: { google: googleOidcProvider({ clientId: "google-client", @@ -383,7 +383,7 @@ describe("WalletClient OIDC redirect auth", () => { }); const badState = encodeOidcState({ nonce: "bad-nonce", - scope: Constants.defaultWaasAuthScope, + scope: "project-id", }); await expect(wallet.completeOidcRedirectAuth({ @@ -448,7 +448,7 @@ describe("WalletClient OIDC redirect auth", () => { }); const state = encodeOidcState({ nonce: "nonce-1", - scope: Constants.defaultWaasAuthScope, + scope: "project-id", }); await expect(wallet.completeOidcRedirectAuth({ @@ -536,10 +536,12 @@ function createWalletClient { const environment = params.environment ?? testEnvironment() as Env; return new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: params.projectId ?? "project-id", environment, storage: new MemoryStorageManager(), redirectAuthStorage: params.redirectAuthStorage, diff --git a/tests/walletAccess.test.ts b/tests/walletAccess.test.ts index 2579e43..33004ba 100644 --- a/tests/walletAccess.test.ts +++ b/tests/walletAccess.test.ts @@ -93,7 +93,8 @@ describe("WalletClient access management", () => { function createWalletWithSession(): WalletClient { const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage: new MemoryStorageManager(), credentialSigner: new MockSigner(), diff --git a/tests/walletErrors.test.ts b/tests/walletErrors.test.ts index 7c5473a..9a85076 100644 --- a/tests/walletErrors.test.ts +++ b/tests/walletErrors.test.ts @@ -32,7 +32,8 @@ afterEach(() => { describe("WalletClient errors", () => { it("wraps local validation failures separately from request failures", async () => { const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage: new MemoryStorageManager(), redirectAuthStorage: new MemoryStorageManager(), @@ -52,7 +53,8 @@ describe("WalletClient errors", () => { vi.stubGlobal("fetch", vi.fn(async () => new Response("Bad Gateway", {status: 502}))); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage: new MemoryStorageManager(), credentialSigner: new MockSigner(), @@ -82,7 +84,8 @@ describe("WalletClient errors", () => { })); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage: new MemoryStorageManager(), credentialSigner: new MockSigner(), diff --git a/tests/walletSession.test.ts b/tests/walletSession.test.ts index 45268d2..aa92521 100644 --- a/tests/walletSession.test.ts +++ b/tests/walletSession.test.ts @@ -2,6 +2,7 @@ import {afterEach, describe, expect, it, vi} from "vitest"; import {WalletClient} from "../src/clients/walletClient"; import type {CredentialSigner} from "../src/credentialSigner"; +import {Networks} from "../src/networks"; import {OMSClient} from "../src/omsClient"; import {MemoryStorageManager} from "../src/storageManager"; import {Constants} from "../src/utils/constants"; @@ -40,7 +41,8 @@ describe("WalletClient session storage", () => { vi.stubGlobal("localStorage", undefined); const client = new OMSClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), credentialSigner: new MockSigner(), }); @@ -57,13 +59,14 @@ describe("WalletClient session storage", () => { storage.set(Constants.sessionEmailStorageKey, "user@example.com"); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage, credentialSigner: new MockSigner(false), }); - await expect(wallet.signMessage({network: "polygon", message: "hello"})).rejects.toMatchObject({ + await expect(wallet.signMessage({network: Networks.polygon, message: "hello"})).rejects.toMatchObject({ code: "OMS_SESSION_MISSING", operation: "wallet.signMessage", message: "No active wallet session", @@ -81,7 +84,8 @@ describe("WalletClient session storage", () => { const redirectAuthStorage = new MemoryStorageManager(); redirectAuthStorage.set(Constants.redirectAuthStorageKey, JSON.stringify({verifier: "old-verifier"})); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage, redirectAuthStorage, @@ -138,7 +142,8 @@ describe("WalletClient session storage", () => { vi.stubGlobal("fetch", fetchMock); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage, redirectAuthStorage, @@ -166,7 +171,8 @@ describe("WalletClient session storage", () => { storage.set(Constants.sessionEmailStorageKey, "user@example.com"); const client = new OMSClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage, credentialSigner: new MockSigner(), @@ -221,7 +227,8 @@ describe("WalletClient session storage", () => { vi.stubGlobal("fetch", fetchMock); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage, credentialSigner: new MockSigner(), @@ -293,7 +300,8 @@ describe("WalletClient session storage", () => { vi.stubGlobal("fetch", fetchMock); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage: new MemoryStorageManager(), credentialSigner: new MockSigner(), @@ -348,7 +356,8 @@ describe("WalletClient session storage", () => { const storage = new MemoryStorageManager(); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage, credentialSigner: new MockSigner(), @@ -397,7 +406,8 @@ describe("WalletClient session storage", () => { vi.stubGlobal("fetch", fetchMock); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage: new MemoryStorageManager(), credentialSigner: new MockSigner(), diff --git a/tests/walletSigning.test.ts b/tests/walletSigning.test.ts index 54c5a56..da75979 100644 --- a/tests/walletSigning.test.ts +++ b/tests/walletSigning.test.ts @@ -2,6 +2,7 @@ import {afterEach, describe, expect, it, vi} from "vitest"; import {WalletClient} from "../src/clients/walletClient"; import type {CredentialSigner} from "../src/credentialSigner"; +import {Networks} from "../src/networks"; import {MemoryStorageManager} from "../src/storageManager"; class MockSigner implements CredentialSigner { @@ -63,7 +64,7 @@ describe("WalletClient signing", () => { const url = input.toString(); const body = JSON.parse(init?.body as string); - expect((init?.headers as Record)["X-Access-Key"]).toBe("project-key"); + expect((init?.headers as Record)["X-Access-Key"]).toBe("public-api-key"); const headers = init?.headers as Record; expect(headers["OMS-Wallet-Signature"]).toContain('alg="ecdsa-p256-sha256"'); expect(headers.Authorization).toBeUndefined(); @@ -83,7 +84,7 @@ describe("WalletClient signing", () => { const wallet = createWalletWithSession("0x1111111111111111111111111111111111111111"); - await expect(wallet.signTypedData({network: "polygon", typedData})).resolves.toBe("0xsigned"); + await expect(wallet.signTypedData({network: Networks.polygon, typedData})).resolves.toBe("0xsigned"); }); it("validates signatures through the generated wallet public client", async () => { @@ -104,7 +105,7 @@ describe("WalletClient signing", () => { const body = JSON.parse(init?.body as string); const headers = init?.headers as Record; - expect(headers["X-Access-Key"]).toBe("project-key"); + expect(headers["X-Access-Key"]).toBe("public-api-key"); expect(headers["OMS-Wallet-Signature"]).toBeUndefined(); expect(headers.Authorization).toBeUndefined(); @@ -135,13 +136,13 @@ describe("WalletClient signing", () => { const wallet = createWalletWithSession("0x1111111111111111111111111111111111111111"); await expect(wallet.isValidMessageSignature({ - network: "polygon", + network: Networks.polygon, message: "hello", signature: "0xmessage", })).resolves.toBe(true); await expect(wallet.isValidTypedDataSignature({ - network: 137n, + network: Networks.polygon, walletAddress: "0x1111111111111111111111111111111111111111", typedData, signature: "0xtyped", @@ -151,7 +152,8 @@ describe("WalletClient signing", () => { function createWalletWithSession(walletAddress: string): WalletClient { const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage: new MemoryStorageManager(), credentialSigner: new MockSigner(), diff --git a/tests/walletTransactions.test.ts b/tests/walletTransactions.test.ts index a226b5e..6b4e985 100644 --- a/tests/walletTransactions.test.ts +++ b/tests/walletTransactions.test.ts @@ -3,6 +3,7 @@ import {afterEach, describe, expect, it, vi} from "vitest"; import {WalletClient} from "../src/clients/walletClient"; import type {CredentialSigner} from "../src/credentialSigner"; import {TransactionStatus} from "../src/generated/waas.gen"; +import {Networks} from "../src/networks"; import {MemoryStorageManager} from "../src/storageManager"; class MockSigner implements CredentialSigner { @@ -138,7 +139,7 @@ describe("WalletClient transactions", () => { ); const response = await wallet.sendTransaction({ - network: "polygon", + network: Networks.polygon, to: "0x1111111111111111111111111111111111111111", value: 0n, selectFeeOption: (feeOptions) => { @@ -187,7 +188,7 @@ describe("WalletClient transactions", () => { ); const response = await wallet.sendTransaction({ - network: "polygon", + network: Networks.polygon, to: "0x1111111111111111111111111111111111111111", value: 0n, waitForStatus: false, @@ -217,7 +218,8 @@ describe("WalletClient transactions", () => { vi.stubGlobal("fetch", fetchMock); const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage: new MemoryStorageManager(), credentialSigner: new MockSigner(), @@ -261,7 +263,7 @@ describe("WalletClient transactions", () => { ); await expect(wallet.sendTransaction({ - network: "polygon", + network: Networks.polygon, to: "0x1111111111111111111111111111111111111111", value: 0n, })).rejects.toMatchObject({ @@ -275,7 +277,8 @@ describe("WalletClient transactions", () => { function createWalletWithSession(storage: MemoryStorageManager, walletAddress: string): WalletClient { const wallet = new WalletClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: testEnvironment(), storage, credentialSigner: new MockSigner(), diff --git a/type-tests/oidcProviderTypes.ts b/type-tests/oidcProviderTypes.ts index de257ea..4129760 100644 --- a/type-tests/oidcProviderTypes.ts +++ b/type-tests/oidcProviderTypes.ts @@ -1,5 +1,14 @@ import {WalletClient, type OidcProviderName} from "../src/clients/walletClient"; -import {OMSClient, type OMSClientSessionLoginType, type OMSClientSessionState} from "../src/index"; +import { + Networks, + OMSClient, + findNetworkById, + findNetworkByName, + supportedNetworks, + type Network, + type OMSClientSessionLoginType, + type OMSClientSessionState, +} from "../src/index"; import {defineOmsEnvironment, type OmsEnvironment} from "../src/omsEnvironment"; import {googleOidcProvider} from "../src/oidc"; @@ -47,11 +56,33 @@ if (false) { }); } -const defaultClient = new OMSClient({projectAccessKey: "project-key"}); +const defaultClient = new OMSClient({ + publicApiKey: "public-api-key", + projectId: "project-id", +}); +// @ts-expect-error publicApiKey is required. +new OMSClient({projectId: "project-id"}); +// @ts-expect-error projectId is required. +new OMSClient({publicApiKey: "public-api-key"}); +// @ts-expect-error old projectAccessKey initializer name is not supported. +new OMSClient({projectAccessKey: "public-api-key", projectId: "project-id"}); +// @ts-expect-error old authorizationScope initializer name is not supported. +new OMSClient({publicApiKey: "public-api-key", authorizationScope: "project-id"}); const session: OMSClientSessionState = defaultClient.wallet.session; const loginType: OMSClientSessionLoginType | undefined = defaultClient.wallet.session.loginType; +const polygonNetwork: Network = Networks.polygon; +const amoyNetwork: Network | undefined = findNetworkById(80002); +const baseNetwork: Network | undefined = findNetworkByName("base"); +const allNetworks: readonly Network[] = supportedNetworks; void session; void loginType; +void polygonNetwork; +void amoyNetwork; +void baseNetwork; +void allNetworks; +void defaultClient.supportedNetworks; +// @ts-expect-error findNetworkById accepts numeric chain IDs only. +findNetworkById("80002"); void defaultClient.wallet.startOidcRedirectAuth({ provider: "google", redirectUri: "https://app.example/auth/callback", @@ -67,7 +98,8 @@ const customEnvironmentWithoutProviders = defineOmsEnvironment({ indexerUrlTemplate: "https://indexer.example/{value}", }); const customClient = new OMSClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", environment: customEnvironmentWithoutProviders, }); let broadlyTypedClient: OMSClient; @@ -80,14 +112,19 @@ void customClient.wallet.startOidcRedirectAuth({ }); function createClient(params: { - projectAccessKey: string; + publicApiKey: string; + projectId: string; environment?: OmsEnvironment; }) { return new OMSClient(params); } -void createClient({projectAccessKey: "project-key"}); void createClient({ - projectAccessKey: "project-key", + publicApiKey: "public-api-key", + projectId: "project-id", +}); +void createClient({ + publicApiKey: "public-api-key", + projectId: "project-id", environment: customEnvironmentWithoutProviders, }); From d89e64e24a8f19d716db89a69a01d2760ac5b244 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 18:25:10 +0300 Subject: [PATCH 02/16] chore: prepare alpha package and examples --- .github/workflows/pages.yml | 2 + API.md | 97 ++++++++++++++++++--------- README.md | 108 ++++++++++++++++++++----------- examples/node/README.md | 4 +- examples/node/package.json | 3 +- examples/node/signInFlow.ts | 20 ++++-- examples/react/.env.example | 3 +- examples/react/README.md | 4 +- examples/react/package.json | 2 +- examples/react/src/main.tsx | 86 +++++++++++++----------- examples/react/src/styles.css | 41 +++++++----- examples/react/src/vite-env.d.ts | 3 +- package.json | 29 +++++++-- pnpm-lock.yaml | 11 ++-- 14 files changed, 264 insertions(+), 149 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a4df170..254c0b9 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -46,6 +46,8 @@ jobs: run: pnpm build:example env: GITHUB_PAGES: 'true' + VITE_OMS_PUBLIC_API_KEY: ${{ secrets.OMS_PUBLIC_API_KEY }} + VITE_OMS_PROJECT_ID: ${{ secrets.OMS_PROJECT_ID }} - name: Stage Pages artifact run: | diff --git a/API.md b/API.md index 2eaf1a9..364518f 100644 --- a/API.md +++ b/API.md @@ -4,6 +4,7 @@ - [OMSClient](#omsclient) - [Constructor](#constructor) + - [supportedNetworks](#supportednetworks) - [WalletClient](#walletclient) - [walletAddress](#walletaddress) - [session](#session) @@ -63,16 +64,20 @@ The top-level entry point for the SDK. ```typescript -import { OMSClient } from 'typescript-sdk' +import { OMSClient } from '@0xsequence/typescript-sdk' -const oms = new OMSClient({ projectAccessKey: 'your-key' }) +const oms = new OMSClient({ + publicApiKey: 'your-public-api-key', + projectId: 'your-project-id', +}) ``` ### Constructor ```typescript new OMSClient(params: { - projectAccessKey: string + publicApiKey: string + projectId: string environment?: OmsEnvironment storage?: StorageManager redirectAuthStorage?: StorageManager @@ -84,7 +89,8 @@ new OMSClient(params: { | Name | Type | Required | Description | |---|---|---|---| -| `projectAccessKey` | `string` | Yes | Your OMS project access key. | +| `publicApiKey` | `string` | Yes | Your OMS public API key. | +| `projectId` | `string` | Yes | Your OMS project ID. Used as the WaaS signing scope for wallet requests and OIDC redirect state. | | `environment` | `OmsEnvironment` | No | API endpoint configuration. Defaults to the SDK's configured OMS endpoints. | | `storage` | `StorageManager` | No | Storage backend for wallet metadata. Defaults to `LocalStorageManager` when browser `localStorage` is available, otherwise `MemoryStorageManager`. | | `redirectAuthStorage` | `StorageManager` | No | Transient storage for OIDC redirect verifier/state. Defaults to `sessionStorage` when available. | @@ -96,8 +102,15 @@ new OMSClient(params: { |---|---|---| | `wallet` | `WalletClient` | Handles authentication, signing, and transactions. | | `indexer` | `IndexerClient` | Queries on-chain state and token balances. | +| `supportedNetworks` | `readonly Network[]` | Networks configured by the SDK. Same value as the exported `supportedNetworks`. | ---- +### supportedNetworks + +```typescript +oms.supportedNetworks: readonly Network[] +``` + +Returns the supported network registry. Each entry has `id`, `name`, `nativeTokenSymbol`, and `explorerUrl`. ## WalletClient @@ -337,7 +350,7 @@ Signs an arbitrary message using the active wallet session credential. | Name | Type | Description | |---|---|---| -| `network` | `Network` | The network for the signing context. Accepts a chain name string, chain ID bigint, or viem `Chain` object. See [Network](#network). | +| `network` | `Network` | The network for the signing context. Use an exported registry value such as `Networks.polygon`. See [Network](#network). | | `message` | `string` | The message to sign. | **Returns** `Promise` — a hex-encoded signature. @@ -345,11 +358,8 @@ Signs an arbitrary message using the active wallet session credential. **Example** ```typescript -const sig = await oms.wallet.signMessage({ network: 'polygon', message: '0xdeadbeef' }) - -// Using a viem Chain object -import { polygon } from 'viem/chains' -const sig = await oms.wallet.signMessage({ network: polygon, message: '0xdeadbeef' }) +import { Networks } from '@0xsequence/typescript-sdk' +const sigFromNetwork = await oms.wallet.signMessage({ network: Networks.polygon, message: '0xdeadbeef' }) ``` --- @@ -421,13 +431,15 @@ Fetches the latest WaaS status for a prepared/executed transaction. This is usef sendTransaction(params: SendNativeTransactionParams): Promise ``` -Sends native tokens (ETH, MATIC, etc.) to an address. +Sends native tokens (ETH, POL, etc.) to an address. ```typescript +import { parseUnits } from 'viem' + const tx = await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xRecipient', - value: 1_000_000_000_000_000_000n, // 1 MATIC in wei + value: parseUnits('1', 18), // 1 POL }) ``` @@ -441,7 +453,7 @@ Sends a transaction with arbitrary calldata as a hex string. Use this when you h ```typescript const tx = await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xContract', data: '0xa9059cbb000000000000000000000000...', }) @@ -456,6 +468,8 @@ sendTransaction(params: SendContractTransactionParams } } @@ -742,8 +778,7 @@ interface OmsEnvironment { | Field | Type | Description | |---|---|---| | `walletApiUrl` | `string` | Base URL of the WaaS Wallet RPC host. | -| `indexerUrlTemplate` | `string` | URL template for the Indexer API. `{value}` is replaced with the chain ID at request time, e.g. `"https://indexer.example.com/{value}"`. | -| `auth.waasAuthScope` | `string` | WaaS credential auth scope used in signed wallet API requests. Defaults to `proj_1`. | +| `indexerUrlTemplate` | `string` | URL template for the Indexer API. `{value}` is replaced with the supported network name when known, otherwise the provided chain ID, e.g. `"https://indexer.example.com/{value}"`. | | `auth.oidcProviders` | `Record` | OIDC provider configurations addressable by provider key. | The default is exported as `defaultOmsEnvironment` and includes the `google` OIDC provider. diff --git a/README.md b/README.md index 091b91f..5baddd6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A TypeScript SDK for the OMS (Open Money Stack) platform. Provides email and OID ## Usage -This SDK is not published as an npm package yet. In this repository it is consumed as the local pnpm workspace package `typescript-sdk`. +For local development in this repository, install dependencies and build the workspace package: From the repository root: @@ -26,9 +26,13 @@ pnpm dev:example ## Quick Start ```typescript -import { OMSClient } from 'typescript-sdk' +import { Networks, OMSClient } from '@0xsequence/typescript-sdk' +import { parseUnits } from 'viem' -const oms = new OMSClient({ projectAccessKey: 'your-project-access-key' }) +const oms = new OMSClient({ + publicApiKey: 'your-public-api-key', + projectId: 'your-project-id', +}) // 1. Send a one-time code to the user's email await oms.wallet.startEmailAuth({ email: 'user@example.com' }) @@ -42,9 +46,9 @@ console.log('Credential:', credential.credentialId) // 4. Send a transaction const tx = await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xRecipient', - value: 1_000_000_000_000_000_000n, // 1 MATIC + value: parseUnits('1', 18), // 1 POL }) console.log(tx.txnHash ?? tx.txnId) ``` @@ -95,7 +99,10 @@ await oms.wallet.createWallet({ type: WalletType.Ethereum }) Google redirect auth is configured on the default environment. The redirect auth APIs are provider-neutral, so custom environments can add or replace providers. ```typescript -const oms = new OMSClient({ projectAccessKey: 'your-key' }) +const oms = new OMSClient({ + publicApiKey: 'your-public-api-key', + projectId: 'your-project-id', +}) ``` For routers such as React Router or Next.js, use the explicit start/complete methods: @@ -125,20 +132,38 @@ Pending redirect state is stored in `sessionStorage` by default. Final wallet se ## Networks -The `network` parameter on all transaction and signing methods accepts any of three forms: +The SDK exports `Networks`, `supportedNetworks`, `findNetworkById(id)`, and `findNetworkByName(name)` for the networks currently configured by OMS. Each network has `id`, `name`, `nativeTokenSymbol`, and `explorerUrl`. + +The `network` parameter on all transaction and signing methods accepts a `Network` from the SDK registry: ```typescript -// Chain name string -await oms.wallet.signMessage({ network: 'polygon', message: '0xdeadbeef' }) +import { Networks, findNetworkById, supportedNetworks } from '@0xsequence/typescript-sdk' -// Chain ID as bigint -await oms.wallet.signMessage({ network: 137n, message: '0xdeadbeef' }) +await oms.wallet.signMessage({ network: Networks.polygon, message: '0xdeadbeef' }) -// viem Chain object -import { polygon } from 'viem/chains' -await oms.wallet.signMessage({ network: polygon, message: '0xdeadbeef' }) +console.log(supportedNetworks) +console.log(findNetworkById(80002)) // Networks.amoy ``` +| Key | id | name | native token | explorerUrl | +|---|---:|---|---|---| +| `Networks.mainnet` | 1 | `mainnet` | ETH | `https://etherscan.io` | +| `Networks.sepolia` | 11155111 | `sepolia` | ETH | `https://sepolia.etherscan.io` | +| `Networks.polygon` | 137 | `polygon` | POL | `https://polygonscan.com` | +| `Networks.amoy` | 80002 | `amoy` | POL | `https://amoy.polygonscan.com` | +| `Networks.arbitrum` | 42161 | `arbitrum` | ETH | `https://arbiscan.io` | +| `Networks.arbitrumSepolia` | 421614 | `arbitrum-sepolia` | ETH | `https://sepolia.arbiscan.io` | +| `Networks.optimism` | 10 | `optimism` | ETH | `https://optimistic.etherscan.io` | +| `Networks.optimismSepolia` | 11155420 | `optimism-sepolia` | ETH | `https://sepolia-optimism.etherscan.io` | +| `Networks.base` | 8453 | `base` | ETH | `https://basescan.org` | +| `Networks.baseSepolia` | 84532 | `base-sepolia` | ETH | `https://sepolia.basescan.org` | +| `Networks.bsc` | 56 | `bsc` | BNB | `https://bscscan.com` | +| `Networks.bscTestnet` | 97 | `bsc-testnet` | BNB | `https://testnet.bscscan.com` | +| `Networks.arbitrumNova` | 42170 | `arbitrum-nova` | ETH | `https://nova.arbiscan.io` | +| `Networks.avalanche` | 43114 | `avalanche` | AVAX | `https://subnets.avax.network/c-chain` | +| `Networks.avalancheTestnet` | 43113 | `avalanche-testnet` | AVAX | `https://subnets-test.avax.network/c-chain` | +| `Networks.katana` | 747474 | `katana` | ETH | `https://katanascan.com` | + ## Sending Transactions `sendTransaction` has three overloaded signatures to cover the most common patterns. @@ -146,10 +171,12 @@ await oms.wallet.signMessage({ network: polygon, message: '0xdeadbeef' }) ### Native Token Transfer ```typescript +import { parseUnits } from 'viem' + const tx = await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xRecipient', - value: 1_000_000_000_000_000_000n, // 1 MATIC in wei + value: parseUnits('1', 18), // 1 POL }) ``` @@ -157,7 +184,7 @@ const tx = await oms.wallet.sendTransaction({ ```typescript const tx = await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xContract', data: '0xa9059cbb000000000000000000000000...', }) @@ -168,6 +195,8 @@ const tx = await oms.wallet.sendTransaction({ Pass an ABI and function name — the SDK encodes the calldata automatically using viem. ```typescript +import { parseUnits } from 'viem' + const erc20Abi = [ { name: 'transfer', @@ -180,11 +209,11 @@ const erc20Abi = [ ] as const const tx = await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xTokenContract', abi: erc20Abi, functionName: 'transfer', - args: ['0xRecipient', 1_000_000_000_000_000_000n], + args: ['0xRecipient', parseUnits('1', 18)], }) ``` @@ -197,10 +226,12 @@ To return immediately after execute without status polling, pass returned `txnId`. ```typescript +import { parseUnits } from 'viem' + const tx = await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xRecipient', - value: 1n, + value: parseUnits('0.001', 18), waitForStatus: false, }) @@ -210,10 +241,12 @@ const status = await oms.wallet.getTransactionStatus({ txnId: tx.txnId }) To tune polling, pass `statusPolling`: ```typescript +import { parseUnits } from 'viem' + await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xRecipient', - value: 1n, + value: parseUnits('0.001', 18), statusPolling: { timeoutMs: 30_000, intervalMs: 1_000, @@ -227,7 +260,7 @@ available. ```typescript const tx = await oms.wallet.sendTransaction({ - network: 'polygon', + network: Networks.polygon, to: '0xTokenContract', data: '0xa9059cbb000000000000000000000000...', selectFeeOption: async (feeOptions) => { @@ -243,13 +276,11 @@ const tx = await oms.wallet.sendTransaction({ ```typescript const oms = new OMSClient({ - projectAccessKey: 'your-key', + publicApiKey: 'your-public-api-key', + projectId: 'your-project-id', environment: { walletApiUrl: 'https://staging-wallet.example.com', indexerUrlTemplate: 'https://staging-indexer.example.com/{value}', - auth: { - waasAuthScope: 'proj_1', - }, }, }) ``` @@ -259,10 +290,11 @@ const oms = new OMSClient({ The default storage backend is browser `localStorage` when available, otherwise in-memory storage for wallet metadata only. The default browser signer stores its non-extractable key reference separately through WebCrypto-compatible browser storage. Provide a custom `StorageManager` for persistent Node.js, React Native, or testing sessions: ```typescript -import { MemoryStorageManager, OMSClient } from 'typescript-sdk' +import { MemoryStorageManager, OMSClient } from '@0xsequence/typescript-sdk' const oms = new OMSClient({ - projectAccessKey: 'your-key', + publicApiKey: 'your-public-api-key', + projectId: 'your-project-id', storage: new MemoryStorageManager(), }) ``` @@ -275,7 +307,7 @@ OIDC redirect auth uses separate transient storage for verifier/state data. In b ```typescript const signature = await oms.wallet.signMessage({ - network: 'polygon', + network: Networks.polygon, message: '0xdeadbeef', }) ``` @@ -284,12 +316,12 @@ const signature = await oms.wallet.signMessage({ ```typescript const signature = await oms.wallet.signTypedData({ - network: 'polygon', + network: Networks.polygon, typedData, }) const isValid = await oms.wallet.isValidTypedDataSignature({ - network: 'polygon', + network: Networks.polygon, walletAddress: oms.wallet.walletAddress, typedData, signature, @@ -299,13 +331,15 @@ const isValid = await oms.wallet.isValidTypedDataSignature({ ### Call a Contract (method string + args) ```typescript +import { parseUnits } from 'viem' + const tx = await oms.wallet.callContract({ - network: 'polygon', + network: Networks.polygon, contractAddress: '0xTokenContract', method: 'transfer(address,uint256)', args: [ { type: 'address', value: '0xRecipient' }, - { type: 'uint256', value: '1000000000000000000' }, + { type: 'uint256', value: parseUnits('1', 18).toString() }, ], }) ``` @@ -354,10 +388,10 @@ await oms.wallet.signOut() ### Handle SDK Errors ```typescript -import { OmsSdkError } from 'typescript-sdk' +import { OmsSdkError } from '@0xsequence/typescript-sdk' try { - await oms.wallet.signMessage({ network: 'polygon', message: '0xdeadbeef' }) + await oms.wallet.signMessage({ network: Networks.polygon, message: '0xdeadbeef' }) } catch (err) { if (err instanceof OmsSdkError) { if (err.code === 'OMS_AUTH_COMMITMENT_CONSUMED') { diff --git a/examples/node/README.md b/examples/node/README.md index 69109d5..0e6f534 100644 --- a/examples/node/README.md +++ b/examples/node/README.md @@ -3,7 +3,7 @@ This example consumes the SDK as a workspace package: ```ts -import { MemoryStorageManager, OMSClient } from 'typescript-sdk' +import { MemoryStorageManager, Networks, OMSClient } from '@0xsequence/typescript-sdk' ``` Run it from the repository root: @@ -11,7 +11,7 @@ Run it from the repository root: ```bash pnpm install pnpm build -pnpm dev:node-example +OMS_PUBLIC_API_KEY=your-public-api-key OMS_PROJECT_ID=your-project-id pnpm dev:node-example ``` The example prompts for an email address, sends an OTP code, then prompts for the code. diff --git a/examples/node/package.json b/examples/node/package.json index 46fd607..3584606 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -8,8 +8,7 @@ "build": "tsc --noEmit" }, "dependencies": { - "typescript-sdk": "workspace:*", - "viem": "^2.48.4" + "@0xsequence/typescript-sdk": "workspace:*" }, "devDependencies": { "@types/node": "^22.19.19", diff --git a/examples/node/signInFlow.ts b/examples/node/signInFlow.ts index ea5edc0..d446f4f 100644 --- a/examples/node/signInFlow.ts +++ b/examples/node/signInFlow.ts @@ -1,14 +1,14 @@ import readline from "node:readline/promises"; -import {MemoryStorageManager, OMSClient} from "typescript-sdk"; -import {polygonAmoy} from "viem/chains"; +import {MemoryStorageManager, Networks, OMSClient} from "@0xsequence/typescript-sdk"; -const projectAccessKey = "AQAAAAAAAAK2JvvZhWqZ51riasWBftkrVXE"; +const publicApiKey = requiredEnv("OMS_PUBLIC_API_KEY", process.env.OMS_PUBLIC_API_KEY); +const projectId = requiredEnv("OMS_PROJECT_ID", process.env.OMS_PROJECT_ID); async function main() { console.log("------------------------------------------------------------"); console.log(" OmsWallet sign-in flow"); console.log("------------------------------------------------------------"); - console.log("project access key :", mask(projectAccessKey)); + console.log("public API key :", mask(publicApiKey)); console.log(); const email = await prompt("Enter your email: "); @@ -17,7 +17,8 @@ async function main() { console.log("[setup] creating OmsWallet…"); const client = new OMSClient({ - projectAccessKey, + publicApiKey, + projectId, storage: new MemoryStorageManager(), }); @@ -56,7 +57,7 @@ async function main() { console.log("✓ sign-in flow complete"); await client.wallet.signMessage({ - network: polygonAmoy, + network: Networks.amoy, message: "test" }); } @@ -67,6 +68,13 @@ function mask(value: string | undefined): string { return `${value.slice(0, 4)}…${value.slice(-4)}`; } +function requiredEnv(name: string, value: string | undefined): string { + if (!value) { + throw new Error(`Missing ${name}. Set it before running pnpm dev:node-example`); + } + return value; +} + async function prompt(question: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await rl.question(question); diff --git a/examples/react/.env.example b/examples/react/.env.example index 5304b04..0ad1393 100644 --- a/examples/react/.env.example +++ b/examples/react/.env.example @@ -1 +1,2 @@ -VITE_OMS_PROJECT_ACCESS_KEY=AQAAAAAAAAK2JvvZhWqZ51riasWBftkrVXE +VITE_OMS_PUBLIC_API_KEY=your-public-api-key +VITE_OMS_PROJECT_ID=your-oms-project-id diff --git a/examples/react/README.md b/examples/react/README.md index 03561ae..a6f194f 100644 --- a/examples/react/README.md +++ b/examples/react/README.md @@ -3,7 +3,7 @@ This example consumes the SDK as a workspace package: ```ts -import { OMSClient } from 'typescript-sdk' +import { OMSClient } from '@0xsequence/typescript-sdk' ``` Run it from the repository root: @@ -18,7 +18,7 @@ The dev server runs at `http://localhost:5173`. The deployed example is available at `https://0xsequence.github.io/typescript-sdk/react-example`. -The example includes a default demo project access key. To override it locally: +The example requires a public API key and project ID. Configure them locally before running the dev server: ```bash cp examples/react/.env.example examples/react/.env.local diff --git a/examples/react/package.json b/examples/react/package.json index 92bf4ce..ffb2870 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -11,7 +11,7 @@ "dependencies": { "react": "^19.2.5", "react-dom": "^19.2.5", - "typescript-sdk": "workspace:*" + "@0xsequence/typescript-sdk": "workspace:*" }, "devDependencies": { "@types/react": "^19.2.14", diff --git a/examples/react/src/main.tsx b/examples/react/src/main.tsx index f9614a5..7f2e0bb 100644 --- a/examples/react/src/main.tsx +++ b/examples/react/src/main.tsx @@ -1,46 +1,40 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' import { + Networks, OMSClient, type FeeOptionSelection, type FeeOptionWithBalance, + type Network, type OMSClientSessionLoginType, -} from 'typescript-sdk' +} from '@0xsequence/typescript-sdk' import './styles.css' type Step = 'email' | 'code' | 'wallet' -type DemoNetworkId = 'polygon' | 'amoy' type FeeSelectionController = { resolve: (selection: FeeOptionSelection) => void reject: (error: Error) => void } -const NETWORKS: Array<{ - id: DemoNetworkId - label: string - explorerTxUrl: string -}> = [ - { - id: 'polygon', - label: 'Polygon', - explorerTxUrl: 'https://polygonscan.com/tx/', - }, - { - id: 'amoy', - label: 'Polygon Amoy', - explorerTxUrl: 'https://amoy.polygonscan.com/tx/', - }, -] +const NETWORKS: readonly Network[] = Object.values(Networks) const DEFAULT_MESSAGE = 'test' const DEFAULT_TX_TO = '0xE5E8B483FfC05967FcFed58cc98D053265af6D99' -const PROJECT_ACCESS_KEY = import.meta.env.VITE_OMS_PROJECT_ACCESS_KEY ?? 'AQAAAAAAAAK2JvvZhWqZ51riasWBftkrVXE' +const PUBLIC_API_KEY = requiredEnv('VITE_OMS_PUBLIC_API_KEY', import.meta.env.VITE_OMS_PUBLIC_API_KEY) +const PROJECT_ID = requiredEnv('VITE_OMS_PROJECT_ID', import.meta.env.VITE_OMS_PROJECT_ID) + +function requiredEnv(name: string, value: string | undefined): string { + if (!value) { + throw new Error(`Missing ${name}. Copy examples/react/.env.example to examples/react/.env.local and set it.`) + } + return value +} function App() { const [step, setStep] = useState('email') const [email, setEmail] = useState('') const [code, setCode] = useState('') const [message, setMessage] = useState(DEFAULT_MESSAGE) - const [selectedNetworkId, setSelectedNetworkId] = useState('amoy') + const [selectedNetworkId, setSelectedNetworkId] = useState(Networks.amoy.id) const [transactionTo, setTransactionTo] = useState(DEFAULT_TX_TO) const [transactionValue, setTransactionValue] = useState('0') const [walletAddress, setWalletAddress] = useState('') @@ -57,10 +51,11 @@ function App() { const oms = useMemo(() => { return new OMSClient({ - projectAccessKey: PROJECT_ACCESS_KEY, + publicApiKey: PUBLIC_API_KEY, + projectId: PROJECT_ID, }) }, []) - const selectedNetwork = NETWORKS.find(network => network.id === selectedNetworkId) ?? NETWORKS[1] + const selectedNetwork = NETWORKS.find(network => network.id === selectedNetworkId) ?? Networks.amoy const session = oms.wallet.session useEffect(() => { @@ -144,7 +139,7 @@ function App() { async function signMessage() { await run('Signing message...', setWalletStatus, async () => { const signature = await oms.wallet.signMessage({ - network: selectedNetwork.id, + network: selectedNetwork, message, }) setLastSignature(signature) @@ -158,13 +153,13 @@ function App() { setLastTransactionExplorerUrl('') try { const tx = await oms.wallet.sendTransaction({ - network: selectedNetwork.id, + network: selectedNetwork, to: transactionTo as `0x${string}`, value: BigInt(transactionValue || '0'), selectFeeOption: waitForFeeOptionSelection, }) setLastTransactionHash(tx.txnHash ?? tx.txnId) - setLastTransactionExplorerUrl(tx.txnHash ? `${selectedNetwork.explorerTxUrl}${tx.txnHash}` : '') + setLastTransactionExplorerUrl(tx.txnHash ? transactionExplorerUrl(selectedNetwork, tx.txnHash) : '') setWalletStatus('Transaction sent.') } finally { feeSelection.current = null @@ -307,22 +302,23 @@ function App() { -
-

Network

-
+
+
+

Network

+ {selectedNetwork.nativeTokenSymbol} +
+
@@ -418,6 +414,18 @@ createRoot(document.getElementById('root')!).render( , ) +function transactionExplorerUrl(network: Network, txnHash: string): string { + return `${network.explorerUrl.replace(/\/+$/, '')}/tx/${txnHash}` +} + +function networkLabel(network: Network): string { + const label = network.name + .split('-') + .map(part => part.toUpperCase() === 'BSC' ? part.toUpperCase() : part[0].toUpperCase() + part.slice(1)) + .join(' ') + return `${label} (${network.id})` +} + function formatLoginType(loginType: OMSClientSessionLoginType | undefined): string { switch (loginType) { case 'email': diff --git a/examples/react/src/styles.css b/examples/react/src/styles.css index 2cae8bb..6fe051e 100644 --- a/examples/react/src/styles.css +++ b/examples/react/src/styles.css @@ -15,7 +15,8 @@ body { } button, -input { +input, +select { font: inherit; } @@ -79,7 +80,8 @@ label { font-size: 14px; } -input { +input, +select { width: 100%; min-height: 44px; padding: 10px 12px; @@ -89,7 +91,8 @@ input { background: #ffffff; } -input:focus { +input:focus, +select:focus { outline: 2px solid #7aa7ff; outline-offset: 1px; border-color: #6b9dff; @@ -162,24 +165,32 @@ button:disabled { line-height: 1.25; } -.segmented { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 4px; - padding: 4px; - border-radius: 6px; - background: #eef2f7; +.network-tool { + gap: 8px; } -.segmented button { - min-height: 38px; - color: #344054; - background: transparent; +.tool-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; } -.segmented button[aria-checked="true"] { +.network-meta { + min-width: 48px; + padding: 4px 8px; + border-radius: 6px; color: #ffffff; background: #1d4ed8; + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-align: center; +} + +select:disabled { + color: #667085; + background: #eef2f7; } .fee-options { diff --git a/examples/react/src/vite-env.d.ts b/examples/react/src/vite-env.d.ts index 151f14d..aa51999 100644 --- a/examples/react/src/vite-env.d.ts +++ b/examples/react/src/vite-env.d.ts @@ -1,7 +1,8 @@ /// interface ImportMetaEnv { - readonly VITE_OMS_PROJECT_ACCESS_KEY?: string + readonly VITE_OMS_PUBLIC_API_KEY?: string + readonly VITE_OMS_PROJECT_ID?: string } interface ImportMeta { diff --git a/package.json b/package.json index 2a9be9d..17b8640 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,15 @@ { - "name": "typescript-sdk", - "version": "1.0.0", - "description": "", + "name": "@0xsequence/typescript-sdk", + "version": "0.1.0-alpha.0", + "description": "TypeScript SDK for OMS (Open Money Stack).", "main": "dist/index.js", "module": "dist/esm/index.js", "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md", + "API.md" + ], "exports": { ".": { "types": "./dist/index.d.ts", @@ -12,10 +17,25 @@ "require": "./dist/index.js" } }, + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/typescript-sdk.git" + }, + "bugs": { + "url": "https://github.com/0xsequence/typescript-sdk/issues" + }, + "homepage": "https://github.com/0xsequence/typescript-sdk#readme", + "publishConfig": { + "access": "public", + "tag": "alpha" + }, "packageManager": "pnpm@11.1.3", "scripts": { "clean": "rm -rf dist", "build": "pnpm clean && tsc && tsc -p tsconfig.esm.json && node scripts/write-esm-package.cjs", + "prepack": "pnpm build", + "prepublishOnly": "pnpm exec tsc --noEmit && pnpm test", "dev:example": "pnpm --filter react-example dev", "build:example": "pnpm --filter react-example build", "dev:node-example": "pnpm --filter node-example dev", @@ -31,6 +51,5 @@ "dotenv": "^17.4.2", "typescript": "^5.5.3", "vitest": "^4.1.5" - }, - "private": true + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93ef064..41423d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,12 +24,9 @@ importers: examples/node: dependencies: - typescript-sdk: + '@0xsequence/typescript-sdk': specifier: workspace:* version: link:../.. - viem: - specifier: ^2.48.4 - version: 2.48.4(typescript@5.9.3) devDependencies: '@types/node': specifier: ^22.19.19 @@ -43,15 +40,15 @@ importers: examples/react: dependencies: + '@0xsequence/typescript-sdk': + specifier: workspace:* + version: link:../.. react: specifier: ^19.2.5 version: 19.2.5 react-dom: specifier: ^19.2.5 version: 19.2.5(react@19.2.5) - typescript-sdk: - specifier: workspace:* - version: link:../.. devDependencies: '@types/react': specifier: ^19.2.14 From d919a7b355659cb2abfb13900ca43996a3a2139e Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 18:36:26 +0300 Subject: [PATCH 03/16] fix: use numeric indexer chain ids --- AGENTS.md | 8 ++++---- API.md | 8 ++++---- README.md | 2 +- src/clients/indexerClient.ts | 16 ++++++---------- src/clients/walletClient.ts | 22 +++++++++++----------- tests/indexerClient.test.ts | 8 +++++--- type-tests/oidcProviderTypes.ts | 22 ++++++++++++++++++++++ 7 files changed, 53 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 848cf29..da1c279 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Overview -This repository is a pnpm workspace for the OMS TypeScript SDK. The root package exports the `typescript-sdk` library used by the React and Node examples. The SDK covers wallet authentication, OIDC redirect auth, signed WaaS requests, wallet/session storage, transaction submission, signing, access management, and indexer balance queries. +This repository is a pnpm workspace for the OMS TypeScript SDK. The root package exports the `@0xsequence/typescript-sdk` library used by the React and Node examples. The SDK covers wallet authentication, OIDC redirect auth, signed WaaS requests, wallet/session storage, transaction submission, signing, access management, and indexer balance queries. ## Setup and Tooling @@ -56,7 +56,7 @@ This repository is a pnpm workspace for the OMS TypeScript SDK. The root package - Route wallet API calls through `WalletClient`, generated WaaS types, `createSignedFetch`, and `CredentialSigner` instead of duplicating signing or header logic. - Use `StorageManager` abstractions for persistence-sensitive code. Browser storage and memory fallback behavior are part of the SDK contract. - Preserve typed SDK error classes and `toOmsSdkError` behavior when wrapping network, generated-client, validation, session, and transaction-status failures. -- Keep network names and chain IDs going through `NetworkBindings` instead of ad hoc string conversion. +- Keep supported network metadata and chain ID lookup going through `src/networks.ts`, `Networks`, `supportedNetworks`, `findNetworkById`, and `findNetworkByName` instead of ad hoc conversion. - The TypeScript compiler is the enforced style gate. There is no separate lint or formatter command in the root scripts, so avoid broad formatting churn and match the local file style. ## Testing Guidance @@ -78,9 +78,9 @@ This repository is a pnpm workspace for the OMS TypeScript SDK. The root package ## Security and Configuration - Do not commit real secrets. `.env.local` and `.env.*.local` files are ignored for local overrides. -- The React example uses `examples/react/.env.example` for `VITE_OMS_PROJECT_ACCESS_KEY`; keep local overrides in `examples/react/.env.local`. +- The React example uses `examples/react/.env.example` for `VITE_OMS_PUBLIC_API_KEY` and `VITE_OMS_PROJECT_ID`; keep local overrides in `examples/react/.env.local`. - Treat credential signing, nonce handling, OIDC redirect state cleanup, session persistence, transaction execution/status polling, and access revocation as high-risk paths. Prefer focused regression tests for changes in these areas. -- CI may provide `OMS_PROJECT_ACCESS_KEY` for tests. Do not require that secret for ordinary local unit tests unless the test explicitly needs an external boundary. +- GitHub Pages may provide `OMS_PUBLIC_API_KEY` and `OMS_PROJECT_ID` secrets for the deployed React example. Do not require those secrets for ordinary local unit tests unless the test explicitly needs an external boundary. ## Agent Workflow Rules diff --git a/API.md b/API.md index 364518f..423715d 100644 --- a/API.md +++ b/API.md @@ -631,7 +631,7 @@ Accessed via `oms.indexer`. Queries on-chain token balances through the OMS Inde ```typescript getTokenBalances(params: { - chainId: string + chainId: number contractAddress: string walletAddress: string includeMetadata: boolean @@ -644,7 +644,7 @@ Fetches token balances for a wallet on a given chain and contract (first page, u | Name | Type | Description | |---|---|---| -| `chainId` | `string` | Numeric chain ID or supported network name, e.g. `"137"` or `"polygon"`. | +| `chainId` | `number` | Numeric chain ID, e.g. `137` for Polygon or `1` for Ethereum mainnet. | | `contractAddress` | `string` | The token contract address to query. | | `walletAddress` | `string` | The wallet address whose balances to fetch. Use `oms.wallet.walletAddress` after checking it is defined. | | `includeMetadata` | `boolean` | When `true`, the response includes token metadata such as name, symbol, and decimals. | @@ -658,7 +658,7 @@ const { walletAddress } = oms.wallet if (!walletAddress) throw new Error('No active wallet session') const result = await oms.indexer.getTokenBalances({ - chainId: '137', + chainId: 137, contractAddress: '0xTokenContract', walletAddress, includeMetadata: true, @@ -675,7 +675,7 @@ for (const b of result.balances) { ```typescript getNativeTokenBalance(params: { - chainId: string + chainId: number walletAddress: string }): Promise ``` diff --git a/README.md b/README.md index 5baddd6..33ae96f 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ const { walletAddress } = oms.wallet if (!walletAddress) throw new Error('No active wallet session') const result = await oms.indexer.getTokenBalances({ - chainId: '137', + chainId: 137, contractAddress: '0xTokenContract', walletAddress, includeMetadata: true, diff --git a/src/clients/indexerClient.ts b/src/clients/indexerClient.ts index 6c50eaa..e0b96a1 100644 --- a/src/clients/indexerClient.ts +++ b/src/clients/indexerClient.ts @@ -1,7 +1,7 @@ // Converted from Swift IndexerClient. import {HttpClient} from "../httpClient.js"; -import {findNetworkById, findNetworkByName} from "../networks.js"; +import {findNetworkById} from "../networks.js"; import {errorMessage, OmsRequestError, OmsResponseError} from "../errors.js"; export interface TokenBalancesPage { @@ -88,7 +88,7 @@ export class IndexerClient { } async getTokenBalances(params: { - chainId: string + chainId: number contractAddress: string walletAddress: string includeMetadata: boolean @@ -118,7 +118,7 @@ export class IndexerClient { } async getNativeTokenBalance(params: { - chainId: string + chainId: number walletAddress: string }): Promise { const response = await this.postJson("indexer.getNativeTokenBalance", { @@ -187,16 +187,12 @@ export class IndexerClient { return {statusCode: response.statusCode, payload}; } - private indexerUrl(chainId: string): string { + private indexerUrl(chainId: number): string { return this.environment.indexerUrlTemplate.replace("{value}", this.indexerNetworkValue(chainId)); } - private indexerNetworkValue(chainId: string): string { - const normalized = chainId.trim().toLowerCase(); - if (/^\d+$/.test(normalized)) { - return findNetworkById(Number(normalized))?.name ?? normalized; - } - return findNetworkByName(normalized)?.name ?? normalized; + private indexerNetworkValue(chainId: number): string { + return findNetworkById(chainId)?.name ?? chainId.toString(); } private defaultHeaders(): Record { diff --git a/src/clients/walletClient.ts b/src/clients/walletClient.ts index 45424fb..8e0131d 100644 --- a/src/clients/walletClient.ts +++ b/src/clients/walletClient.ts @@ -548,7 +548,7 @@ export class WalletClient { return this.runOperation("wallet.signMessage", async () => { await this.requireActiveSession("wallet.signMessage") const request: SignMessageRequest = { - network: this.parseWalletNetwork(params.network), + network: this.parseWalletNetwork(params.network).toString(), walletId: this.walletId, message: params.message, } @@ -561,7 +561,7 @@ export class WalletClient { return this.runOperation("wallet.signTypedData", async () => { await this.requireActiveSession("wallet.signTypedData") const request: SignTypedDataRequest = { - network: this.parseWalletNetwork(params.network), + network: this.parseWalletNetwork(params.network).toString(), walletId: this.walletId, typedData: normalizeJsonBigInts(params.typedData), } @@ -573,7 +573,7 @@ export class WalletClient { async isValidMessageSignature(params: IsValidMessageSignatureParams): Promise { return this.runOperation("wallet.isValidMessageSignature", async () => { const request: IsValidMessageSignatureRequest = { - network: params.network === undefined ? undefined : this.parseWalletNetwork(params.network), + network: params.network === undefined ? undefined : this.parseWalletNetwork(params.network).toString(), walletAddress: params.walletAddress, walletId: params.walletId ?? (params.walletAddress ? undefined : this.activeWalletId()), message: params.message, @@ -587,7 +587,7 @@ export class WalletClient { async isValidTypedDataSignature(params: IsValidTypedDataSignatureParams): Promise { return this.runOperation("wallet.isValidTypedDataSignature", async () => { const request: IsValidTypedDataSignatureRequest = { - network: params.network === undefined ? undefined : this.parseWalletNetwork(params.network), + network: params.network === undefined ? undefined : this.parseWalletNetwork(params.network).toString(), walletAddress: params.walletAddress, walletId: params.walletId ?? (params.walletAddress ? undefined : this.activeWalletId()), typedData: normalizeJsonBigInts(params.typedData), @@ -613,7 +613,7 @@ export class WalletClient { : params.data const request: PrepareEthereumTransactionRequest = { - network: this.parseWalletNetwork(params.network), + network: this.parseWalletNetwork(params.network).toString(), walletId: this.walletId, to: params.to, value: (params.value ?? 0n).toString(), @@ -645,7 +645,7 @@ export class WalletClient { return this.runOperation("wallet.callContract", async () => { await this.requireActiveSession("wallet.callContract") const request: PrepareEthereumContractCallRequest = { - network: this.parseWalletNetwork(params.network), + network: this.parseWalletNetwork(params.network).toString(), walletId: this.walletId, contract: params.contractAddress, method: params.method, @@ -1139,7 +1139,7 @@ export class WalletClient { balance: '0', blockHash: undefined, blockNumber: undefined, - chainId: Number(this.parseWalletNetwork(network)), + chainId: this.parseWalletNetwork(network), }).catch(() => undefined) } @@ -1209,12 +1209,12 @@ export class WalletClient { return this.walletId || undefined } - private parseWalletNetwork(network: Network): string { - return network.id.toString() + private parseWalletNetwork(network: Network): number { + return network.id } - private parseIndexerNetwork(network: Network): string { - return network.name + private parseIndexerNetwork(network: Network): number { + return network.id } private isNativeToken(feeOption: FeeOption): boolean { diff --git a/tests/indexerClient.test.ts b/tests/indexerClient.test.ts index f6749a2..1bea182 100644 --- a/tests/indexerClient.test.ts +++ b/tests/indexerClient.test.ts @@ -9,7 +9,8 @@ afterEach(() => { describe("IndexerClient errors", () => { it("wraps invalid JSON responses in typed SDK errors", async () => { - vi.stubGlobal("fetch", vi.fn(async () => new Response("not-json", {status: 200}))); + const fetchMock = vi.fn(async () => new Response("not-json", {status: 200})); + vi.stubGlobal("fetch", fetchMock); const indexer = new IndexerClient({ publicApiKey: "public-api-key", @@ -17,7 +18,7 @@ describe("IndexerClient errors", () => { }); await expect(indexer.getTokenBalances({ - chainId: "137", + chainId: 137, contractAddress: "0x2222222222222222222222222222222222222222", walletAddress: "0x9999999999999999999999999999999999999999", includeMetadata: false, @@ -26,6 +27,7 @@ describe("IndexerClient errors", () => { operation: "indexer.getTokenBalances", status: 200, }); + expect(fetchMock.mock.calls[0][0].toString()).toBe("https://indexer.example/polygon/GetTokenBalances"); }); it("wraps non-JSON HTTP responses as retryable HTTP errors", async () => { @@ -37,7 +39,7 @@ describe("IndexerClient errors", () => { }); await expect(indexer.getTokenBalances({ - chainId: "137", + chainId: 137, contractAddress: "0x2222222222222222222222222222222222222222", walletAddress: "0x9999999999999999999999999999999999999999", includeMetadata: false, diff --git a/type-tests/oidcProviderTypes.ts b/type-tests/oidcProviderTypes.ts index 4129760..f2ec416 100644 --- a/type-tests/oidcProviderTypes.ts +++ b/type-tests/oidcProviderTypes.ts @@ -83,6 +83,28 @@ void allNetworks; void defaultClient.supportedNetworks; // @ts-expect-error findNetworkById accepts numeric chain IDs only. findNetworkById("80002"); +void defaultClient.indexer.getTokenBalances({ + chainId: 137, + contractAddress: "0x2222222222222222222222222222222222222222", + walletAddress: "0x9999999999999999999999999999999999999999", + includeMetadata: false, +}); +void defaultClient.indexer.getTokenBalances({ + // @ts-expect-error Indexer chain IDs are numeric. + chainId: "137", + contractAddress: "0x2222222222222222222222222222222222222222", + walletAddress: "0x9999999999999999999999999999999999999999", + includeMetadata: false, +}); +void defaultClient.indexer.getNativeTokenBalance({ + chainId: 137, + walletAddress: "0x9999999999999999999999999999999999999999", +}); +void defaultClient.indexer.getNativeTokenBalance({ + // @ts-expect-error Indexer chain IDs are numeric. + chainId: "137", + walletAddress: "0x9999999999999999999999999999999999999999", +}); void defaultClient.wallet.startOidcRedirectAuth({ provider: "google", redirectUri: "https://app.example/auth/callback", From 3fc54c809dcea2a172aa97c70a1cf30c23af664e Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 18:39:29 +0300 Subject: [PATCH 04/16] fix: use network registry in indexer api --- API.md | 8 ++++---- README.md | 2 +- src/clients/indexerClient.ts | 9 +++++---- src/clients/walletClient.ts | 8 ++------ tests/indexerClient.test.ts | 5 +++-- type-tests/oidcProviderTypes.ts | 24 ++++++++++++++++++------ 6 files changed, 33 insertions(+), 23 deletions(-) diff --git a/API.md b/API.md index 423715d..8b4e0ee 100644 --- a/API.md +++ b/API.md @@ -631,7 +631,7 @@ Accessed via `oms.indexer`. Queries on-chain token balances through the OMS Inde ```typescript getTokenBalances(params: { - chainId: number + network: Network contractAddress: string walletAddress: string includeMetadata: boolean @@ -644,7 +644,7 @@ Fetches token balances for a wallet on a given chain and contract (first page, u | Name | Type | Description | |---|---|---| -| `chainId` | `number` | Numeric chain ID, e.g. `137` for Polygon or `1` for Ethereum mainnet. | +| `network` | `Network` | The network to query. Use an exported registry value such as `Networks.polygon`. | | `contractAddress` | `string` | The token contract address to query. | | `walletAddress` | `string` | The wallet address whose balances to fetch. Use `oms.wallet.walletAddress` after checking it is defined. | | `includeMetadata` | `boolean` | When `true`, the response includes token metadata such as name, symbol, and decimals. | @@ -658,7 +658,7 @@ const { walletAddress } = oms.wallet if (!walletAddress) throw new Error('No active wallet session') const result = await oms.indexer.getTokenBalances({ - chainId: 137, + network: Networks.polygon, contractAddress: '0xTokenContract', walletAddress, includeMetadata: true, @@ -675,7 +675,7 @@ for (const b of result.balances) { ```typescript getNativeTokenBalance(params: { - chainId: number + network: Network walletAddress: string }): Promise ``` diff --git a/README.md b/README.md index 33ae96f..264e609 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ const { walletAddress } = oms.wallet if (!walletAddress) throw new Error('No active wallet session') const result = await oms.indexer.getTokenBalances({ - chainId: 137, + network: Networks.polygon, contractAddress: '0xTokenContract', walletAddress, includeMetadata: true, diff --git a/src/clients/indexerClient.ts b/src/clients/indexerClient.ts index e0b96a1..b67aa21 100644 --- a/src/clients/indexerClient.ts +++ b/src/clients/indexerClient.ts @@ -3,6 +3,7 @@ import {HttpClient} from "../httpClient.js"; import {findNetworkById} from "../networks.js"; import {errorMessage, OmsRequestError, OmsResponseError} from "../errors.js"; +import type {Network} from "../networks.js"; export interface TokenBalancesPage { page: number; @@ -88,7 +89,7 @@ export class IndexerClient { } async getTokenBalances(params: { - chainId: number + network: Network contractAddress: string walletAddress: string includeMetadata: boolean @@ -101,7 +102,7 @@ export class IndexerClient { }; const bodyString = JSON.stringify(request); - const baseUrl = this.indexerUrl(params.chainId); + const baseUrl = this.indexerUrl(params.network.id); const response = await this.postJson("indexer.getTokenBalances", { baseUrl, @@ -118,11 +119,11 @@ export class IndexerClient { } async getNativeTokenBalance(params: { - chainId: number + network: Network walletAddress: string }): Promise { const response = await this.postJson("indexer.getNativeTokenBalance", { - baseUrl: this.indexerUrl(params.chainId), + baseUrl: this.indexerUrl(params.network.id), path: "/GetNativeTokenBalance", body: JSON.stringify({ accountAddress: params.walletAddress }), headers: this.defaultHeaders(), diff --git a/src/clients/walletClient.ts b/src/clients/walletClient.ts index 8e0131d..bd7534a 100644 --- a/src/clients/walletClient.ts +++ b/src/clients/walletClient.ts @@ -1114,7 +1114,7 @@ export class WalletClient { walletAddress: Address, ): Promise { return this.indexerClient.getNativeTokenBalance({ - chainId: this.parseIndexerNetwork(network), + network, walletAddress, }).catch(() => undefined) } @@ -1125,7 +1125,7 @@ export class WalletClient { walletAddress: Address, ): Promise { return this.indexerClient.getTokenBalances({ - chainId: this.parseIndexerNetwork(network), + network, contractAddress, walletAddress, includeMetadata: false, @@ -1213,10 +1213,6 @@ export class WalletClient { return network.id } - private parseIndexerNetwork(network: Network): number { - return network.id - } - private isNativeToken(feeOption: FeeOption): boolean { return feeOption.token.type.toLowerCase() === 'native' || (!feeOption.token.contractAddress && !feeOption.token.tokenID) diff --git a/tests/indexerClient.test.ts b/tests/indexerClient.test.ts index 1bea182..e57e826 100644 --- a/tests/indexerClient.test.ts +++ b/tests/indexerClient.test.ts @@ -1,6 +1,7 @@ import {afterEach, describe, expect, it, vi} from "vitest"; import {IndexerClient} from "../src/clients/indexerClient"; +import {Networks} from "../src/networks"; afterEach(() => { vi.restoreAllMocks(); @@ -18,7 +19,7 @@ describe("IndexerClient errors", () => { }); await expect(indexer.getTokenBalances({ - chainId: 137, + network: Networks.polygon, contractAddress: "0x2222222222222222222222222222222222222222", walletAddress: "0x9999999999999999999999999999999999999999", includeMetadata: false, @@ -39,7 +40,7 @@ describe("IndexerClient errors", () => { }); await expect(indexer.getTokenBalances({ - chainId: 137, + network: Networks.polygon, contractAddress: "0x2222222222222222222222222222222222222222", walletAddress: "0x9999999999999999999999999999999999999999", includeMetadata: false, diff --git a/type-tests/oidcProviderTypes.ts b/type-tests/oidcProviderTypes.ts index f2ec416..e7c1a3e 100644 --- a/type-tests/oidcProviderTypes.ts +++ b/type-tests/oidcProviderTypes.ts @@ -84,25 +84,37 @@ void defaultClient.supportedNetworks; // @ts-expect-error findNetworkById accepts numeric chain IDs only. findNetworkById("80002"); void defaultClient.indexer.getTokenBalances({ - chainId: 137, + network: Networks.polygon, contractAddress: "0x2222222222222222222222222222222222222222", walletAddress: "0x9999999999999999999999999999999999999999", includeMetadata: false, }); void defaultClient.indexer.getTokenBalances({ - // @ts-expect-error Indexer chain IDs are numeric. - chainId: "137", + // @ts-expect-error Indexer public methods accept Network objects, not numeric chain IDs. + network: 137, contractAddress: "0x2222222222222222222222222222222222222222", walletAddress: "0x9999999999999999999999999999999999999999", includeMetadata: false, }); -void defaultClient.indexer.getNativeTokenBalance({ +void defaultClient.indexer.getTokenBalances({ + // @ts-expect-error chainId is not a public indexer parameter. chainId: 137, + contractAddress: "0x2222222222222222222222222222222222222222", + walletAddress: "0x9999999999999999999999999999999999999999", + includeMetadata: false, +}); +void defaultClient.indexer.getNativeTokenBalance({ + network: Networks.polygon, + walletAddress: "0x9999999999999999999999999999999999999999", +}); +void defaultClient.indexer.getNativeTokenBalance({ + // @ts-expect-error Indexer public methods accept Network objects, not numeric chain IDs. + network: 137, walletAddress: "0x9999999999999999999999999999999999999999", }); void defaultClient.indexer.getNativeTokenBalance({ - // @ts-expect-error Indexer chain IDs are numeric. - chainId: "137", + // @ts-expect-error chainId is not a public indexer parameter. + chainId: 137, walletAddress: "0x9999999999999999999999999999999999999999", }); void defaultClient.wallet.startOidcRedirectAuth({ From 58892f4f87a33f243d4ba57b5615a69d731ca549 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 18:42:10 +0300 Subject: [PATCH 05/16] refactor: remove redundant network helpers --- examples/react/src/main.tsx | 6 +++--- src/clients/indexerClient.ts | 13 ++++--------- src/clients/walletClient.ts | 18 +++++++----------- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/examples/react/src/main.tsx b/examples/react/src/main.tsx index 7f2e0bb..b4f43cf 100644 --- a/examples/react/src/main.tsx +++ b/examples/react/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import { Networks, OMSClient, + supportedNetworks, type FeeOptionSelection, type FeeOptionWithBalance, type Network, @@ -16,7 +17,6 @@ type FeeSelectionController = { reject: (error: Error) => void } -const NETWORKS: readonly Network[] = Object.values(Networks) const DEFAULT_MESSAGE = 'test' const DEFAULT_TX_TO = '0xE5E8B483FfC05967FcFed58cc98D053265af6D99' const PUBLIC_API_KEY = requiredEnv('VITE_OMS_PUBLIC_API_KEY', import.meta.env.VITE_OMS_PUBLIC_API_KEY) @@ -55,7 +55,7 @@ function App() { projectId: PROJECT_ID, }) }, []) - const selectedNetwork = NETWORKS.find(network => network.id === selectedNetworkId) ?? Networks.amoy + const selectedNetwork = supportedNetworks.find(network => network.id === selectedNetworkId) ?? Networks.amoy const session = oms.wallet.session useEffect(() => { @@ -313,7 +313,7 @@ function App() { onChange={(event) => setSelectedNetworkId(Number(event.target.value))} disabled={isBusy} > - {NETWORKS.map(network => ( + {supportedNetworks.map(network => ( diff --git a/src/clients/indexerClient.ts b/src/clients/indexerClient.ts index b67aa21..42674ad 100644 --- a/src/clients/indexerClient.ts +++ b/src/clients/indexerClient.ts @@ -1,7 +1,6 @@ // Converted from Swift IndexerClient. import {HttpClient} from "../httpClient.js"; -import {findNetworkById} from "../networks.js"; import {errorMessage, OmsRequestError, OmsResponseError} from "../errors.js"; import type {Network} from "../networks.js"; @@ -102,7 +101,7 @@ export class IndexerClient { }; const bodyString = JSON.stringify(request); - const baseUrl = this.indexerUrl(params.network.id); + const baseUrl = this.indexerUrl(params.network); const response = await this.postJson("indexer.getTokenBalances", { baseUrl, @@ -123,7 +122,7 @@ export class IndexerClient { walletAddress: string }): Promise { const response = await this.postJson("indexer.getNativeTokenBalance", { - baseUrl: this.indexerUrl(params.network.id), + baseUrl: this.indexerUrl(params.network), path: "/GetNativeTokenBalance", body: JSON.stringify({ accountAddress: params.walletAddress }), headers: this.defaultHeaders(), @@ -188,12 +187,8 @@ export class IndexerClient { return {statusCode: response.statusCode, payload}; } - private indexerUrl(chainId: number): string { - return this.environment.indexerUrlTemplate.replace("{value}", this.indexerNetworkValue(chainId)); - } - - private indexerNetworkValue(chainId: number): string { - return findNetworkById(chainId)?.name ?? chainId.toString(); + private indexerUrl(network: Network): string { + return this.environment.indexerUrlTemplate.replace("{value}", network.name); } private defaultHeaders(): Record { diff --git a/src/clients/walletClient.ts b/src/clients/walletClient.ts index bd7534a..d59e613 100644 --- a/src/clients/walletClient.ts +++ b/src/clients/walletClient.ts @@ -548,7 +548,7 @@ export class WalletClient { return this.runOperation("wallet.signMessage", async () => { await this.requireActiveSession("wallet.signMessage") const request: SignMessageRequest = { - network: this.parseWalletNetwork(params.network).toString(), + network: params.network.id.toString(), walletId: this.walletId, message: params.message, } @@ -561,7 +561,7 @@ export class WalletClient { return this.runOperation("wallet.signTypedData", async () => { await this.requireActiveSession("wallet.signTypedData") const request: SignTypedDataRequest = { - network: this.parseWalletNetwork(params.network).toString(), + network: params.network.id.toString(), walletId: this.walletId, typedData: normalizeJsonBigInts(params.typedData), } @@ -573,7 +573,7 @@ export class WalletClient { async isValidMessageSignature(params: IsValidMessageSignatureParams): Promise { return this.runOperation("wallet.isValidMessageSignature", async () => { const request: IsValidMessageSignatureRequest = { - network: params.network === undefined ? undefined : this.parseWalletNetwork(params.network).toString(), + network: params.network?.id.toString(), walletAddress: params.walletAddress, walletId: params.walletId ?? (params.walletAddress ? undefined : this.activeWalletId()), message: params.message, @@ -587,7 +587,7 @@ export class WalletClient { async isValidTypedDataSignature(params: IsValidTypedDataSignatureParams): Promise { return this.runOperation("wallet.isValidTypedDataSignature", async () => { const request: IsValidTypedDataSignatureRequest = { - network: params.network === undefined ? undefined : this.parseWalletNetwork(params.network).toString(), + network: params.network?.id.toString(), walletAddress: params.walletAddress, walletId: params.walletId ?? (params.walletAddress ? undefined : this.activeWalletId()), typedData: normalizeJsonBigInts(params.typedData), @@ -613,7 +613,7 @@ export class WalletClient { : params.data const request: PrepareEthereumTransactionRequest = { - network: this.parseWalletNetwork(params.network).toString(), + network: params.network.id.toString(), walletId: this.walletId, to: params.to, value: (params.value ?? 0n).toString(), @@ -645,7 +645,7 @@ export class WalletClient { return this.runOperation("wallet.callContract", async () => { await this.requireActiveSession("wallet.callContract") const request: PrepareEthereumContractCallRequest = { - network: this.parseWalletNetwork(params.network).toString(), + network: params.network.id.toString(), walletId: this.walletId, contract: params.contractAddress, method: params.method, @@ -1139,7 +1139,7 @@ export class WalletClient { balance: '0', blockHash: undefined, blockNumber: undefined, - chainId: this.parseWalletNetwork(network), + chainId: network.id, }).catch(() => undefined) } @@ -1209,10 +1209,6 @@ export class WalletClient { return this.walletId || undefined } - private parseWalletNetwork(network: Network): number { - return network.id - } - private isNativeToken(feeOption: FeeOption): boolean { return feeOption.token.type.toLowerCase() === 'native' || (!feeOption.token.contractAddress && !feeOption.token.tokenID) From e5208377e334bff62baeefa7d8cdf24f6c6fc570 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 18:48:41 +0300 Subject: [PATCH 06/16] docs: align release docs with sdk api --- API.md | 4 ++-- README.md | 4 ++++ examples/node/signInFlow.ts | 4 ++-- examples/react/README.md | 3 +++ src/index.ts | 5 +++++ type-tests/oidcProviderTypes.ts | 7 +++++++ 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index 8b4e0ee..76e7aec 100644 --- a/API.md +++ b/API.md @@ -638,7 +638,7 @@ getTokenBalances(params: { }): Promise ``` -Fetches token balances for a wallet on a given chain and contract (first page, up to 40 entries). +Fetches token balances for a wallet on a given network and contract (first page, up to 40 entries). **Parameters** @@ -778,7 +778,7 @@ interface OmsEnvironment { | Field | Type | Description | |---|---|---| | `walletApiUrl` | `string` | Base URL of the WaaS Wallet RPC host. | -| `indexerUrlTemplate` | `string` | URL template for the Indexer API. `{value}` is replaced with the supported network name when known, otherwise the provided chain ID, e.g. `"https://indexer.example.com/{value}"`. | +| `indexerUrlTemplate` | `string` | URL template for the Indexer API. `{value}` is replaced with the selected network name, e.g. `"https://indexer.example.com/{value}"`. | | `auth.oidcProviders` | `Record` | OIDC provider configurations addressable by provider key. | The default is exported as `defaultOmsEnvironment` and includes the `google` OIDC provider. diff --git a/README.md b/README.md index 264e609..e620551 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ A deployed React example is available at [https://0xsequence.github.io/typescrip To run it locally from the repository root: ```bash +cp examples/react/.env.example examples/react/.env.local +# Fill VITE_OMS_PUBLIC_API_KEY and VITE_OMS_PROJECT_ID in examples/react/.env.local pnpm dev:example ``` @@ -285,6 +287,8 @@ const oms = new OMSClient({ }) ``` +For indexer requests, `{value}` is replaced with the selected `Network.name`, such as `polygon` or `amoy`. + ### Custom Storage and Signing The default storage backend is browser `localStorage` when available, otherwise in-memory storage for wallet metadata only. The default browser signer stores its non-extractable key reference separately through WebCrypto-compatible browser storage. Provide a custom `StorageManager` for persistent Node.js, React Native, or testing sessions: diff --git a/examples/node/signInFlow.ts b/examples/node/signInFlow.ts index d446f4f..bfc5714 100644 --- a/examples/node/signInFlow.ts +++ b/examples/node/signInFlow.ts @@ -24,7 +24,7 @@ async function main() { console.log("[setup] ready:", client.wallet.constructor.name); console.log(); - console.log(`[step 1] signInWithEmail("${email}")`); + console.log(`[step 1] startEmailAuth("${email}")`); let t = Date.now(); try { @@ -39,7 +39,7 @@ async function main() { const code = await prompt("Enter the code from your email: "); - console.log(`[step 2] completeEmailSignIn("${mask(code)}")`); + console.log(`[step 2] completeEmailAuth("${mask(code)}")`); t = Date.now(); try { diff --git a/examples/react/README.md b/examples/react/README.md index a6f194f..ec9c8d0 100644 --- a/examples/react/README.md +++ b/examples/react/README.md @@ -11,6 +11,8 @@ Run it from the repository root: ```bash pnpm install pnpm build +cp examples/react/.env.example examples/react/.env.local +# Fill VITE_OMS_PUBLIC_API_KEY and VITE_OMS_PROJECT_ID in examples/react/.env.local pnpm dev:example ``` @@ -22,6 +24,7 @@ The example requires a public API key and project ID. Configure them locally bef ```bash cp examples/react/.env.example examples/react/.env.local +# Fill VITE_OMS_PUBLIC_API_KEY and VITE_OMS_PROJECT_ID in examples/react/.env.local ``` Google/OIDC redirect sign-in uses the SDK default Google client id. diff --git a/src/index.ts b/src/index.ts index 34f7fe9..4b5a552 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,6 +70,11 @@ export type { StartOidcRedirectAuthResult, WalletActivationResult, } from './clients/walletClient.js' +export type { + TokenBalance, + TokenBalancesPage, + TokenBalancesResult, +} from './clients/indexerClient.js' export type { AccessGrant, AccessGrantPage, diff --git a/type-tests/oidcProviderTypes.ts b/type-tests/oidcProviderTypes.ts index e7c1a3e..2be6193 100644 --- a/type-tests/oidcProviderTypes.ts +++ b/type-tests/oidcProviderTypes.ts @@ -8,6 +8,9 @@ import { type Network, type OMSClientSessionLoginType, type OMSClientSessionState, + type TokenBalance, + type TokenBalancesPage, + type TokenBalancesResult, } from "../src/index"; import {defineOmsEnvironment, type OmsEnvironment} from "../src/omsEnvironment"; import {googleOidcProvider} from "../src/oidc"; @@ -74,12 +77,16 @@ const polygonNetwork: Network = Networks.polygon; const amoyNetwork: Network | undefined = findNetworkById(80002); const baseNetwork: Network | undefined = findNetworkByName("base"); const allNetworks: readonly Network[] = supportedNetworks; +const tokenBalance: TokenBalance = {chainId: Networks.polygon.id}; +const tokenBalancesPage: TokenBalancesPage = {page: 0, pageSize: 40, more: false}; +const tokenBalancesResult: TokenBalancesResult = {status: 200, page: tokenBalancesPage, balances: [tokenBalance]}; void session; void loginType; void polygonNetwork; void amoyNetwork; void baseNetwork; void allNetworks; +void tokenBalancesResult; void defaultClient.supportedNetworks; // @ts-expect-error findNetworkById accepts numeric chain IDs only. findNetworkById("80002"); From 618a266710723094661282962e5b64de37f13bda Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 18:56:27 +0300 Subject: [PATCH 07/16] fix: allow all-token indexer balance queries --- API.md | 12 ++++++++---- README.md | 3 ++- src/clients/indexerClient.ts | 18 ++++++++++++++---- tests/indexerClient.test.ts | 33 ++++++++++++++++++++++++++++++++- type-tests/oidcProviderTypes.ts | 6 ++++++ 5 files changed, 62 insertions(+), 10 deletions(-) diff --git a/API.md b/API.md index 76e7aec..0cd022a 100644 --- a/API.md +++ b/API.md @@ -632,22 +632,27 @@ Accessed via `oms.indexer`. Queries on-chain token balances through the OMS Inde ```typescript getTokenBalances(params: { network: Network - contractAddress: string + contractAddress?: string walletAddress: string includeMetadata: boolean + page?: { + page?: number + pageSize?: number + } }): Promise ``` -Fetches token balances for a wallet on a given network and contract (first page, up to 40 entries). +Fetches token balances for a wallet on a given network. Omit `contractAddress` to query balances across contracts; provide it to filter to one token contract. The default request returns page `0` with up to `40` entries. **Parameters** | Name | Type | Description | |---|---|---| | `network` | `Network` | The network to query. Use an exported registry value such as `Networks.polygon`. | -| `contractAddress` | `string` | The token contract address to query. | +| `contractAddress` | `string` | Optional token contract filter. Omit to query balances across contracts. | | `walletAddress` | `string` | The wallet address whose balances to fetch. Use `oms.wallet.walletAddress` after checking it is defined. | | `includeMetadata` | `boolean` | When `true`, the response includes token metadata such as name, symbol, and decimals. | +| `page` | `{ page?: number; pageSize?: number }` | Optional pagination request. Defaults to `{ page: 0, pageSize: 40 }`. | **Returns** `Promise` — see [TokenBalancesResult](#tokenbalancesresult). @@ -659,7 +664,6 @@ if (!walletAddress) throw new Error('No active wallet session') const result = await oms.indexer.getTokenBalances({ network: Networks.polygon, - contractAddress: '0xTokenContract', walletAddress, includeMetadata: true, }) diff --git a/README.md b/README.md index e620551..7be930d 100644 --- a/README.md +++ b/README.md @@ -356,7 +356,6 @@ if (!walletAddress) throw new Error('No active wallet session') const result = await oms.indexer.getTokenBalances({ network: Networks.polygon, - contractAddress: '0xTokenContract', walletAddress, includeMetadata: true, }) @@ -366,6 +365,8 @@ for (const b of result.balances) { } ``` +Pass `contractAddress` to filter balances to one token contract. The response is paginated; pass `page` when requesting later pages. + ### Manage Access ```typescript diff --git a/src/clients/indexerClient.ts b/src/clients/indexerClient.ts index 42674ad..2693818 100644 --- a/src/clients/indexerClient.ts +++ b/src/clients/indexerClient.ts @@ -63,7 +63,7 @@ interface RequestPage { interface TokenBalancesRequest { page: RequestPage; - contractAddress: string; + contractAddress?: string; accountAddress: string; includeMetadata: boolean; } @@ -89,16 +89,26 @@ export class IndexerClient { async getTokenBalances(params: { network: Network - contractAddress: string + contractAddress?: string walletAddress: string includeMetadata: boolean + page?: { + page?: number + pageSize?: number + } }): Promise { const request: TokenBalancesRequest = { - page: { page: 0, pageSize: 40, more: false }, - contractAddress: params.contractAddress, + page: { + page: params.page?.page ?? 0, + pageSize: params.page?.pageSize ?? 40, + more: false, + }, accountAddress: params.walletAddress, includeMetadata: params.includeMetadata, }; + if (params.contractAddress) { + request.contractAddress = params.contractAddress; + } const bodyString = JSON.stringify(request); const baseUrl = this.indexerUrl(params.network); diff --git a/tests/indexerClient.test.ts b/tests/indexerClient.test.ts index e57e826..71a5856 100644 --- a/tests/indexerClient.test.ts +++ b/tests/indexerClient.test.ts @@ -8,7 +8,38 @@ afterEach(() => { vi.unstubAllGlobals(); }); -describe("IndexerClient errors", () => { +describe("IndexerClient", () => { + it("omits contractAddress when querying balances across contracts", async () => { + const fetchMock = vi.fn(async () => new Response(JSON.stringify({ + page: {page: 1, pageSize: 25, more: false}, + balances: [], + }), {status: 200})); + vi.stubGlobal("fetch", fetchMock); + + const indexer = new IndexerClient({ + publicApiKey: "public-api-key", + environment: testEnvironment(), + }); + + await expect(indexer.getTokenBalances({ + network: Networks.polygon, + walletAddress: "0x9999999999999999999999999999999999999999", + includeMetadata: true, + page: {page: 1, pageSize: 25}, + })).resolves.toMatchObject({ + status: 200, + page: {page: 1, pageSize: 25, more: false}, + balances: [], + }); + + expect(fetchMock.mock.calls[0][0].toString()).toBe("https://indexer.example/polygon/GetTokenBalances"); + expect(JSON.parse(fetchMock.mock.calls[0][1]?.body as string)).toEqual({ + page: {page: 1, pageSize: 25, more: false}, + accountAddress: "0x9999999999999999999999999999999999999999", + includeMetadata: true, + }); + }); + it("wraps invalid JSON responses in typed SDK errors", async () => { const fetchMock = vi.fn(async () => new Response("not-json", {status: 200})); vi.stubGlobal("fetch", fetchMock); diff --git a/type-tests/oidcProviderTypes.ts b/type-tests/oidcProviderTypes.ts index 2be6193..a4e4cef 100644 --- a/type-tests/oidcProviderTypes.ts +++ b/type-tests/oidcProviderTypes.ts @@ -96,6 +96,12 @@ void defaultClient.indexer.getTokenBalances({ walletAddress: "0x9999999999999999999999999999999999999999", includeMetadata: false, }); +void defaultClient.indexer.getTokenBalances({ + network: Networks.polygon, + walletAddress: "0x9999999999999999999999999999999999999999", + includeMetadata: true, + page: {page: 1, pageSize: 25}, +}); void defaultClient.indexer.getTokenBalances({ // @ts-expect-error Indexer public methods accept Network objects, not numeric chain IDs. network: 137, From 2668be952a228348189a5cdb23bce838d3b1e4a1 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:01:20 +0300 Subject: [PATCH 08/16] fix: preserve indexer token metadata --- API.md | 58 +++++++++++++++++- README.md | 4 +- src/clients/indexerClient.ts | 102 ++++++++++++++++++++++++++++++++ src/index.ts | 3 + tests/indexerClient.test.ts | 27 ++++++++- type-tests/oidcProviderTypes.ts | 14 ++++- 6 files changed, 202 insertions(+), 6 deletions(-) diff --git a/API.md b/API.md index 0cd022a..3cef823 100644 --- a/API.md +++ b/API.md @@ -54,6 +54,8 @@ - [TokenBalancesResult](#tokenbalancesresult) - [TokenBalancesPage](#tokenbalancespage) - [TokenBalance](#tokenbalance) + - [TokenContractInfo](#tokencontractinfo) + - [TokenMetadata](#tokenmetadata) - [AbiArg](#abiarg) - [WalletType](#wallettype) @@ -642,7 +644,7 @@ getTokenBalances(params: { }): Promise ``` -Fetches token balances for a wallet on a given network. Omit `contractAddress` to query balances across contracts; provide it to filter to one token contract. The default request returns page `0` with up to `40` entries. +Fetches token balances for a wallet on a given network. Omit `contractAddress` to query balances across contracts; provide it to filter to one token contract. The default request returns page `0` with up to `40` entries. When `includeMetadata` is `true`, token display data is returned on `contractInfo` and `tokenMetadata`; ERC-20 decimals are available as `contractInfo.decimals`. **Parameters** @@ -1089,9 +1091,16 @@ interface TokenBalance { accountAddress?: string tokenId?: string balance?: string + balanceUSD?: string + priceUSD?: string + priceUpdatedAt?: string blockHash?: string blockNumber?: number chainId?: number + uniqueCollectibles?: string + isSummary?: boolean + contractInfo?: TokenContractInfo + tokenMetadata?: TokenMetadata } ``` @@ -1102,9 +1111,56 @@ interface TokenBalance { | `accountAddress` | `string` | Wallet address this balance belongs to. | | `tokenId` | `string` | For ERC-721/ERC-1155 tokens, the token ID. | | `balance` | `string` | Balance in the token's smallest denomination. | +| `balanceUSD` | `string` | USD value when returned by the Indexer. | +| `priceUSD` | `string` | Token price in USD when returned by the Indexer. | +| `priceUpdatedAt` | `string` | Timestamp for the returned USD price. | | `blockHash` | `string` | Block hash at which this balance was recorded. | | `blockNumber` | `number` | Block number at which this balance was recorded. | | `chainId` | `number` | Numeric chain ID. | +| `uniqueCollectibles` | `string` | Number of unique collectibles represented by a summary row. | +| `isSummary` | `boolean` | Whether the row represents an aggregated collection summary. | +| `contractInfo` | `TokenContractInfo` | Contract display metadata. ERC-20 decimals are exposed as `contractInfo.decimals`. | +| `tokenMetadata` | `TokenMetadata` | Token-level metadata for NFT/collection entries when returned. | + +--- + +### TokenContractInfo + +```typescript +interface TokenContractInfo { + chainId?: number + address?: string + name?: string + type?: string + symbol?: string + decimals?: number + logoURI?: string + extensions?: Record + status?: string +} +``` + +Contract-level metadata returned by the Indexer when `includeMetadata` is `true`. + +--- + +### TokenMetadata + +```typescript +interface TokenMetadata { + chainId?: number + contractAddress?: string + tokenId?: string + name?: string + description?: string + image?: string + decimals?: number + attributes?: Record[] + status?: string +} +``` + +Token-level metadata returned by the Indexer when available. --- diff --git a/README.md b/README.md index 7be930d..c666727 100644 --- a/README.md +++ b/README.md @@ -361,11 +361,11 @@ const result = await oms.indexer.getTokenBalances({ }) for (const b of result.balances) { - console.log(b.contractAddress, b.balance) + console.log(b.contractInfo?.symbol, b.balance, b.contractInfo?.decimals) } ``` -Pass `contractAddress` to filter balances to one token contract. The response is paginated; pass `page` when requesting later pages. +Pass `contractAddress` to filter balances to one token contract. With `includeMetadata: true`, ERC-20 decimals are available as `contractInfo.decimals`. The response is paginated; pass `page` when requesting later pages. ### Manage Access diff --git a/src/clients/indexerClient.ts b/src/clients/indexerClient.ts index 2693818..dd8d875 100644 --- a/src/clients/indexerClient.ts +++ b/src/clients/indexerClient.ts @@ -10,6 +10,61 @@ export interface TokenBalancesPage { more: boolean; } +export interface TokenContractInfo { + chainId?: number; + address?: string; + source?: string; + name?: string; + type?: string; + symbol?: string; + decimals?: number; + logoURI?: string; + deployed?: boolean; + bytecodeHash?: string; + extensions?: Record; + updatedAt?: string; + queuedAt?: string | null; + status?: string; +} + +export interface TokenMetadataAsset { + id?: number; + collectionId?: number; + tokenId?: string; + url?: string; + metadataField?: string; + name?: string; + filesize?: number; + mimeType?: string; + width?: number; + height?: number; + updatedAt?: string; +} + +export interface TokenMetadata { + chainId?: number; + contractAddress?: string; + tokenId?: string; + source?: string; + name?: string; + description?: string; + image?: string; + video?: string; + audio?: string; + properties?: Record; + attributes?: Record[]; + image_data?: string; + external_url?: string; + background_color?: string; + animation_url?: string; + decimals?: number; + updatedAt?: string; + assets?: TokenMetadataAsset[]; + status?: string; + queuedAt?: string | null; + lastFetched?: string; +} + export interface TokenBalance { contractType?: string; contractAddress?: string; @@ -17,9 +72,16 @@ export interface TokenBalance { /** Wire format uses `tokenID`; this field is re-mapped during decoding. */ tokenId?: string; balance?: string; + balanceUSD?: string; + priceUSD?: string; + priceUpdatedAt?: string; blockHash?: string; blockNumber?: number; chainId?: number; + uniqueCollectibles?: string; + isSummary?: boolean; + contractInfo?: TokenContractInfo; + tokenMetadata?: TokenMetadata; } export interface TokenBalancesResult { @@ -50,9 +112,27 @@ interface TokenBalanceRaw { accountAddress?: string; tokenID?: string; // note the wire key balance?: string; + balanceUSD?: string; + priceUSD?: string; + priceUpdatedAt?: string; blockHash?: string; blockNumber?: number; chainId?: number; + uniqueCollectibles?: string; + isSummary?: boolean; + contractInfo?: TokenContractInfo; + tokenMetadata?: TokenMetadataRaw; +} + +interface TokenMetadataRaw extends Omit { + tokenId?: string; + tokenID?: string; + assets?: TokenMetadataAssetRaw[]; +} + +interface TokenMetadataAssetRaw extends Omit { + tokenId?: string; + tokenID?: string; } interface RequestPage { @@ -217,9 +297,31 @@ function mapTokenBalance(raw: TokenBalanceRaw): TokenBalance { accountAddress: raw.accountAddress, tokenId: raw.tokenID, balance: raw.balance, + balanceUSD: raw.balanceUSD, + priceUSD: raw.priceUSD, + priceUpdatedAt: raw.priceUpdatedAt, blockHash: raw.blockHash, blockNumber: raw.blockNumber, chainId: raw.chainId, + uniqueCollectibles: raw.uniqueCollectibles, + isSummary: raw.isSummary, + contractInfo: raw.contractInfo, + tokenMetadata: raw.tokenMetadata ? mapTokenMetadata(raw.tokenMetadata) : undefined, + }; +} + +function mapTokenMetadata(raw: TokenMetadataRaw): TokenMetadata { + const {tokenID, assets, ...metadata} = raw; + return { + ...metadata, + tokenId: raw.tokenId ?? tokenID, + assets: assets?.map(asset => { + const {tokenID: assetTokenID, ...metadataAsset} = asset; + return { + ...metadataAsset, + tokenId: asset.tokenId ?? assetTokenID, + }; + }), }; } diff --git a/src/index.ts b/src/index.ts index 4b5a552..f8ed44b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,9 +71,12 @@ export type { WalletActivationResult, } from './clients/walletClient.js' export type { + TokenContractInfo, TokenBalance, TokenBalancesPage, TokenBalancesResult, + TokenMetadata, + TokenMetadataAsset, } from './clients/indexerClient.js' export type { AccessGrant, diff --git a/tests/indexerClient.test.ts b/tests/indexerClient.test.ts index 71a5856..24d31c5 100644 --- a/tests/indexerClient.test.ts +++ b/tests/indexerClient.test.ts @@ -12,7 +12,21 @@ describe("IndexerClient", () => { it("omits contractAddress when querying balances across contracts", async () => { const fetchMock = vi.fn(async () => new Response(JSON.stringify({ page: {page: 1, pageSize: 25, more: false}, - balances: [], + balances: [{ + contractType: "ERC20", + contractAddress: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + accountAddress: "0x9999999999999999999999999999999999999999", + tokenID: "0", + balance: "141799", + balanceUSD: "0.141799", + priceUSD: "1", + chainId: 137, + contractInfo: { + name: "USDC", + symbol: "USDC", + decimals: 6, + }, + }], }), {status: 200})); vi.stubGlobal("fetch", fetchMock); @@ -29,7 +43,16 @@ describe("IndexerClient", () => { })).resolves.toMatchObject({ status: 200, page: {page: 1, pageSize: 25, more: false}, - balances: [], + balances: [{ + tokenId: "0", + balance: "141799", + balanceUSD: "0.141799", + priceUSD: "1", + contractInfo: { + symbol: "USDC", + decimals: 6, + }, + }], }); expect(fetchMock.mock.calls[0][0].toString()).toBe("https://indexer.example/polygon/GetTokenBalances"); diff --git a/type-tests/oidcProviderTypes.ts b/type-tests/oidcProviderTypes.ts index a4e4cef..f35e373 100644 --- a/type-tests/oidcProviderTypes.ts +++ b/type-tests/oidcProviderTypes.ts @@ -11,6 +11,8 @@ import { type TokenBalance, type TokenBalancesPage, type TokenBalancesResult, + type TokenContractInfo, + type TokenMetadata, } from "../src/index"; import {defineOmsEnvironment, type OmsEnvironment} from "../src/omsEnvironment"; import {googleOidcProvider} from "../src/oidc"; @@ -77,7 +79,15 @@ const polygonNetwork: Network = Networks.polygon; const amoyNetwork: Network | undefined = findNetworkById(80002); const baseNetwork: Network | undefined = findNetworkByName("base"); const allNetworks: readonly Network[] = supportedNetworks; -const tokenBalance: TokenBalance = {chainId: Networks.polygon.id}; +const tokenContractInfo: TokenContractInfo = {symbol: "USDC", decimals: 6}; +const tokenMetadata: TokenMetadata = {tokenId: "0", name: "USDC"}; +const tokenBalance: TokenBalance = { + chainId: Networks.polygon.id, + contractInfo: tokenContractInfo, + tokenMetadata, + balanceUSD: "0.141799", + priceUSD: "1", +}; const tokenBalancesPage: TokenBalancesPage = {page: 0, pageSize: 40, more: false}; const tokenBalancesResult: TokenBalancesResult = {status: 200, page: tokenBalancesPage, balances: [tokenBalance]}; void session; @@ -86,6 +96,8 @@ void polygonNetwork; void amoyNetwork; void baseNetwork; void allNetworks; +void tokenContractInfo; +void tokenMetadata; void tokenBalancesResult; void defaultClient.supportedNetworks; // @ts-expect-error findNetworkById accepts numeric chain IDs only. From eaa40063756dc950e8fc527acfdc66f92c8c5131 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:16:50 +0300 Subject: [PATCH 09/16] docs: remove sign-in redirect comment --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index c666727..d94f389 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,6 @@ await oms.wallet.revokeAccess({ targetCredentialId: grants[0].credentialId }) ```typescript await oms.wallet.signOut() -// Redirect to sign-in screen ``` ### Handle SDK Errors From a6620f3c0d01391acc1dbd7e1237074b5519c2af Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:17:42 +0300 Subject: [PATCH 10/16] docs: remove sdk errors section --- README.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README.md b/README.md index d94f389..098b752 100644 --- a/README.md +++ b/README.md @@ -389,23 +389,6 @@ await oms.wallet.revokeAccess({ targetCredentialId: grants[0].credentialId }) await oms.wallet.signOut() ``` -### Handle SDK Errors - -```typescript -import { OmsSdkError } from '@0xsequence/typescript-sdk' - -try { - await oms.wallet.signMessage({ network: Networks.polygon, message: '0xdeadbeef' }) -} catch (err) { - if (err instanceof OmsSdkError) { - if (err.code === 'OMS_AUTH_COMMITMENT_CONSUMED') { - // Restart the auth flow; this OTP/OIDC commitment has already been used. - } - console.error(err.code, err.operation, err.status, err.txnId, err.retryable) - } -} -``` - ## API Reference See [API.md](./API.md) for the full method and type reference. From d820aff7011aab9cdb5bf63d6ac52abdf49c3c95 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:18:27 +0300 Subject: [PATCH 11/16] docs: add message signature validation example --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 098b752..0433a9f 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,13 @@ const signature = await oms.wallet.signMessage({ network: Networks.polygon, message: '0xdeadbeef', }) + +const isValid = await oms.wallet.isValidMessageSignature({ + network: Networks.polygon, + walletAddress: oms.wallet.walletAddress, + message: '0xdeadbeef', + signature, +}) ``` ### Sign and Validate Typed Data From 9e1b6fa23041d387a2707e5290694d9cb3908910 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:19:16 +0300 Subject: [PATCH 12/16] docs: use plain text signing message --- API.md | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index 3cef823..963d889 100644 --- a/API.md +++ b/API.md @@ -361,7 +361,7 @@ Signs an arbitrary message using the active wallet session credential. ```typescript import { Networks } from '@0xsequence/typescript-sdk' -const sigFromNetwork = await oms.wallet.signMessage({ network: Networks.polygon, message: '0xdeadbeef' }) +const sigFromNetwork = await oms.wallet.signMessage({ network: Networks.polygon, message: 'some message to sing' }) ``` --- diff --git a/README.md b/README.md index 0433a9f..4fb8cbd 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ The `network` parameter on all transaction and signing methods accepts a `Networ ```typescript import { Networks, findNetworkById, supportedNetworks } from '@0xsequence/typescript-sdk' -await oms.wallet.signMessage({ network: Networks.polygon, message: '0xdeadbeef' }) +await oms.wallet.signMessage({ network: Networks.polygon, message: 'some message to sing' }) console.log(supportedNetworks) console.log(findNetworkById(80002)) // Networks.amoy @@ -312,13 +312,13 @@ OIDC redirect auth uses separate transient storage for verifier/state data. In b ```typescript const signature = await oms.wallet.signMessage({ network: Networks.polygon, - message: '0xdeadbeef', + message: 'some message to sing', }) const isValid = await oms.wallet.isValidMessageSignature({ network: Networks.polygon, walletAddress: oms.wallet.walletAddress, - message: '0xdeadbeef', + message: 'some message to sing', signature, }) ``` From 923a19624018079836bdc93d46b42c590b163252 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:19:52 +0300 Subject: [PATCH 13/16] docs: rename message signing example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fb8cbd..224fa50 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,7 @@ OIDC redirect auth uses separate transient storage for verifier/state data. In b ## More Examples -### Sign a Message +### Sign and Validate Message ```typescript const signature = await oms.wallet.signMessage({ From a1c7701836c3ec7283e2227e8a6eff0917f202aa Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:20:41 +0300 Subject: [PATCH 14/16] docs: split authentication flow sections --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 224fa50..a3b37b8 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ console.log(tx.txnHash ?? tx.txnId) OMS supports email-based OTP and OIDC authorization-code PKCE redirect auth. +### Email OTP Auth + Email OTP is a two-step flow: 1. **`startEmailAuth({ email })`** — clears any active session and sends a one-time code to the user's inbox. From fcd3777878d4061034387be6e758fb2c93870277 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:29:33 +0300 Subject: [PATCH 15/16] docs: clarify session and balance examples --- README.md | 57 ++++++++++++++++++++++++++----------------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a3b37b8..441f279 100644 --- a/README.md +++ b/README.md @@ -75,29 +75,6 @@ Email OTP is a two-step flow: 1. **`startEmailAuth({ email })`** — clears any active session and sends a one-time code to the user's inbox. 2. **`completeEmailAuth({ code })`** — verifies the code, then automatically loads an existing wallet or creates a new one if none exists. Returns `{ walletAddress, wallet, wallets, credential }`. -The session stores wallet metadata in the configured storage, including the wallet address, credential expiry, login type, and email returned by the wallet API. Browser storage defaults to `localStorage` when available; non-browser runtimes fall back to in-memory storage unless you provide a custom `StorageManager`. Browser signing defaults to a non-extractable WebCrypto P-256 credential using `ecdsa-p256-sha256`, so the private session key is not written to `localStorage`. Completed auth requests ask WaaS for a one-week session lifetime. - -To end the session, call `await oms.wallet.signOut()`. - -```typescript -const { walletAddress, expiresAt, loginType, sessionEmail } = oms.wallet.session -``` - -Apps that need wallet selection can opt out of automatic activation: - -```typescript -const { wallets, credential } = await oms.wallet.completeEmailAuth({ - code, - autoActivate: false, -}) - -// Show wallets in your UI, then either: -await oms.wallet.useWallet({ walletId: wallets[0].id }) - -// Or create and activate a new wallet: -await oms.wallet.createWallet({ type: WalletType.Ethereum }) -``` - ### OIDC Redirect Auth Google redirect auth is configured on the default environment. The redirect auth APIs are provider-neutral, so custom environments can add or replace providers. @@ -134,6 +111,25 @@ void oms.wallet.signInWithOidcRedirect({ provider: 'google' }) Pending redirect state is stored in `sessionStorage` by default. Final wallet session metadata continues to use the configured SDK storage. +### Session State + +Email and OIDC auth both persist the active wallet session in the configured SDK storage. Browser storage defaults to `localStorage` when available; non-browser runtimes fall back to in-memory storage unless you provide a custom `StorageManager`. Browser signing defaults to a non-extractable WebCrypto P-256 credential using `ecdsa-p256-sha256`, so the private session key is not written to `localStorage`. Completed auth requests ask WaaS for a one-week session lifetime. + +Use `oms.wallet.walletAddress` when you only need the active wallet address. Use `oms.wallet.session` when you also need credential expiry, login type, or the email returned by the wallet API. + +```typescript +const walletAddress = oms.wallet.walletAddress +const { expiresAt, loginType, sessionEmail } = oms.wallet.session +``` + +Pending email OTP and OIDC redirect state are not exposed through `session`; use the auth method results to drive pending UI. + +To end the session, call: + +```typescript +await oms.wallet.signOut() +``` + ## Networks The SDK exports `Networks`, `supportedNetworks`, `findNetworkById(id)`, and `findNetworkByName(name)` for the networks currently configured by OMS. Each network has `id`, `name`, `nativeTokenSymbol`, and `explorerUrl`. @@ -357,7 +353,7 @@ const tx = await oms.wallet.callContract({ }) ``` -### Query Token Balances +### Query Token and Native Balances ```typescript const { walletAddress } = oms.wallet @@ -372,6 +368,13 @@ const result = await oms.indexer.getTokenBalances({ for (const b of result.balances) { console.log(b.contractInfo?.symbol, b.balance, b.contractInfo?.decimals) } + +const nativeBalance = await oms.indexer.getNativeTokenBalance({ + network: Networks.polygon, + walletAddress, +}) + +console.log(nativeBalance?.balance) ``` Pass `contractAddress` to filter balances to one token contract. With `includeMetadata: true`, ERC-20 decimals are available as `contractInfo.decimals`. The response is paginated; pass `page` when requesting later pages. @@ -392,12 +395,6 @@ for await (const page of oms.wallet.listAccessPages({ pageSize: 25 })) { await oms.wallet.revokeAccess({ targetCredentialId: grants[0].credentialId }) ``` -### Sign Out - -```typescript -await oms.wallet.signOut() -``` - ## API Reference See [API.md](./API.md) for the full method and type reference. From 0a537d22e23cf267a8a189805985af63657ac306 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Tue, 19 May 2026 19:55:55 +0300 Subject: [PATCH 16/16] docs: fix api reference accuracy --- API.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index 963d889..582ac8d 100644 --- a/API.md +++ b/API.md @@ -56,6 +56,7 @@ - [TokenBalance](#tokenbalance) - [TokenContractInfo](#tokencontractinfo) - [TokenMetadata](#tokenmetadata) + - [TokenMetadataAsset](#tokenmetadataasset) - [AbiArg](#abiarg) - [WalletType](#wallettype) @@ -180,7 +181,10 @@ completeEmailAuth(params: { code: string walletType?: WalletType autoActivate?: boolean -}): Promise<{ walletAddress: Address; wallet: OmsWallet; wallets: OmsWallet[]; credential: WalletCredential }> +}): Promise< + | { walletAddress: Address; wallet: OmsWallet; wallets: OmsWallet[]; credential: WalletCredential } + | { wallets: OmsWallet[]; credential: WalletCredential } +> ``` Verifies the OTP code and activates a wallet. Must be called after [`startEmailAuth`](#startemailauth). @@ -249,7 +253,10 @@ completeOidcRedirectAuth(params: { cleanUrl?: boolean replaceUrl?: (url: string) => void autoActivate?: boolean -}): Promise<{ walletAddress: Address; wallet: OmsWallet; wallets: OmsWallet[]; credential: WalletCredential }> +}): Promise< + | { walletAddress: Address; wallet: OmsWallet; wallets: OmsWallet[]; credential: WalletCredential } + | { wallets: OmsWallet[]; credential: WalletCredential } +> ``` Completes an OIDC redirect flow by validating the persisted state nonce, exchanging the authorization code with WaaS using a one-week session lifetime, and activating an existing wallet or creating one. Pass `autoActivate: false` to return `{ wallets, credential }` for app-driven wallet selection. `cleanUrl` removes OAuth query parameters after successful completion; outside a browser, pass `replaceUrl`. @@ -302,7 +309,6 @@ Clears the wallet session metadata from storage and clears the active credential ```typescript await oms.wallet.signOut() -// Navigate to sign-in screen ``` --- @@ -1130,12 +1136,17 @@ interface TokenBalance { interface TokenContractInfo { chainId?: number address?: string + source?: string name?: string type?: string symbol?: string decimals?: number logoURI?: string + deployed?: boolean + bytecodeHash?: string extensions?: Record + updatedAt?: string + queuedAt?: string | null status?: string } ``` @@ -1151,12 +1162,24 @@ interface TokenMetadata { chainId?: number contractAddress?: string tokenId?: string + source?: string name?: string description?: string image?: string - decimals?: number + video?: string + audio?: string + properties?: Record attributes?: Record[] + image_data?: string + external_url?: string + background_color?: string + animation_url?: string + decimals?: number + updatedAt?: string + assets?: TokenMetadataAsset[] status?: string + queuedAt?: string | null + lastFetched?: string } ``` @@ -1164,6 +1187,28 @@ Token-level metadata returned by the Indexer when available. --- +### TokenMetadataAsset + +```typescript +interface TokenMetadataAsset { + id?: number + collectionId?: number + tokenId?: string + url?: string + metadataField?: string + name?: string + filesize?: number + mimeType?: string + width?: number + height?: number + updatedAt?: string +} +``` + +Media asset metadata associated with token metadata when returned. + +--- + ### AbiArg ```typescript