From 185f792559dca21bb272a525bbf0bcdc75c81627 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 25 Jun 2026 21:43:37 +0300 Subject: [PATCH 1/8] feat: update native SDKs to alpha.3 --- API.md | 387 ++++---- OmsClientReactNativeSdk.podspec | 2 +- README.md | 80 +- android/build.gradle | 9 +- .../OmsClientReactNativeSdkModule.kt | 403 +++++--- examples/sdk-example/android/build.gradle | 4 +- examples/sdk-example/ios/Podfile.lock | 132 +-- examples/sdk-example/src/App.tsx | 69 +- .../android/build.gradle | 4 +- .../trails-actions-example/ios/Podfile.lock | 134 +-- examples/trails-actions-example/src/App.tsx | 142 ++- ios/OmsClientReactNativeSdk.mm | 358 +++---- ios/OmsClientReactNativeSdkImpl.swift | 879 +++++++----------- package.json | 2 +- src/NativeOmsClientReactNativeSdk.ts | 142 ++- src/client.native.ts | 772 ++++++++------- src/client.ts | 303 +++--- src/index.tsx | 49 +- src/networks.ts | 116 +++ src/types.ts | 67 +- test/client.native.test.js | 266 ++++-- 21 files changed, 2302 insertions(+), 2018 deletions(-) create mode 100644 src/networks.ts diff --git a/API.md b/API.md index 88dd013..c9b4872 100644 --- a/API.md +++ b/API.md @@ -1,6 +1,6 @@ # Public API -This document describes the public TypeScript API for external consumers of +This document describes the public TypeScript API for `@0xsequence/oms-react-native-sdk`. ## Installation @@ -9,36 +9,40 @@ This document describes the public TypeScript API for external consumers of npm install @0xsequence/oms-react-native-sdk ``` -Android resolves `io.github.0xsequence:oms-client-kotlin-sdk:0.1.0-alpha.2`. -iOS resolves `oms-client-swift-sdk` `0.1.0-alpha.2`. +Android resolves `io.github.0xsequence:oms-client-kotlin-sdk:0.1.0-alpha.3`. +iOS resolves `oms-client-swift-sdk` `0.1.0-alpha.3`. -## Configure +## Client ```ts -configure({ +import { OMSClient } from '@0xsequence/oms-react-native-sdk'; + +const oms = new OMSClient({ publishableKey: string; - projectId: string; - environment?: { - walletApiUrl?: string; - apiRpcUrl?: string; - indexerUrlTemplate?: string; - }; -}): Promise +}); ``` -Call `configure` before using wallet, signing, transaction, balance, or access -APIs. `publishableKey` is sent to the native SDKs as the OMS publishable key, and -`projectId` is used for wallet request signing scope and session storage scope. +```ts +type OMSClient = { + wallet: OMSWalletClient; + indexer: OMSIndexerClient; + supportedNetworks: OmsNetwork[]; +}; +``` -## Session +## Wallet + +All wallet APIs are accessed through `oms.wallet`. + +### Session ```ts -getWalletAddress(): Promise -getSession(): Promise -onSessionExpired( +oms.wallet.getWalletAddress(): Promise +oms.wallet.getSession(): Promise +oms.wallet.onSessionExpired( listener: (event: OmsClientSessionExpiredEvent) => void ): EventSubscription -signOut(): Promise +oms.wallet.signOut(): Promise ``` ```ts @@ -55,17 +59,12 @@ type OmsClientSessionExpiredEvent = { }; ``` -`getSession` reports completed wallet-session metadata only. Pending OTP, -redirect verifier state, and signer details are native SDK internals. -`onSessionExpired` emits when the native wallet session expires; use the expired -session snapshot to route users back to sign-in or prefill re-authentication UI. - -## Email Auth +### Email Auth ```ts -startEmailAuth(email: string): Promise +oms.wallet.startEmailAuth(email: string): Promise -completeEmailAuth({ +oms.wallet.completeEmailAuth({ code: string; walletSelection?: 'automatic' | 'manual'; walletType?: 'ethereum'; @@ -73,10 +72,10 @@ completeEmailAuth({ }): Promise ``` -## OIDC ID Token Auth +### OIDC ID Token Auth ```ts -signInWithOidcIdToken({ +oms.wallet.signInWithOidcIdToken({ idToken: string; issuer: string; audience: string; @@ -86,10 +85,7 @@ signInWithOidcIdToken({ }): Promise ``` -Use `walletSelection: 'manual'` when the app should present its own wallet -picker before selecting or creating a wallet. - -## OIDC Redirect Auth +### OIDC Redirect Auth ```ts type OidcProviderConfig = { @@ -110,7 +106,7 @@ OidcProviders.google(params?: { ``` ```ts -startOidcRedirectAuth({ +oms.wallet.startOidcRedirectAuth({ provider: OidcProviderConfig; redirectUri: string; walletType?: 'ethereum'; @@ -123,24 +119,18 @@ startOidcRedirectAuth({ challenge: string; }> -handleOidcRedirectCallback({ +oms.wallet.handleOidcRedirectCallback({ callbackUrl?: string | null; walletSelection?: 'automatic' | 'manual'; sessionLifetimeSeconds?: number | null; }): Promise ``` -Apps own browser opening and deep-link handling. Open `authorizationUrl` with -system auth browser UI such as Custom Tabs or ASWebAuthenticationSession, then -pass the resulting app-link URL to `handleOidcRedirectCallback`. +Apps own browser opening and deep-link handling. Open `authorizationUrl` with a +system auth browser, then pass the resulting app-link URL to +`handleOidcRedirectCallback`. -When `relayRedirectUri` is omitted, the provider default is used. Pass -`relayRedirectUri: null` to explicitly use the app `redirectUri` directly. -For Google OIDC providers, `loginHint` is sent as OAuth `login_hint`; if omitted, -the native SDKs may reuse the previous session email during re-authentication. -Auth completion methods default to a one-week session lifetime. - -## Auth Results +### Auth Results ```ts type OmsCompleteAuthResult = @@ -159,18 +149,14 @@ type OmsCompleteAuthResult = credential: OmsCredentialInfo; pendingSelection: OmsPendingWalletSelection; }; -``` -```ts type OmsOidcRedirectAuthResult = | { type: 'completed'; wallet: OmsWallet } | { type: 'walletSelection'; pendingSelection: OmsPendingWalletSelection } | { type: 'notOidcRedirectCallback' } | { type: 'noPendingAuth' } | { type: 'failed'; message: string }; -``` -```ts type OmsPendingWalletSelection = { id: string; walletType: 'ethereum'; @@ -183,19 +169,15 @@ type OmsPendingWalletSelection = { }; ``` -In automatic mode, successful auth selects the first existing wallet matching -`walletType`, or creates and selects one when none exists. In manual mode, no -wallet is selected or created until the app calls a pending-selection method. - -## Wallets +### Wallets ```ts -listWallets(): Promise -useWallet(walletId: string): Promise -createWallet({ +oms.wallet.listWallets(): Promise +oms.wallet.useWallet(walletId: string): Promise +oms.wallet.createWallet({ walletType?: 'ethereum'; reference?: string | null; -}): Promise +} = {}): Promise ``` ```ts @@ -212,41 +194,23 @@ type OmsWalletActivationResult = { }; ``` -## Networks +### Signing ```ts -getSupportedNetworks(): Promise -``` +oms.wallet.signMessage(chainId: string, message: string): Promise -```ts -type OmsNetwork = { - chainId: string; - name: string; - nativeTokenSymbol: string; - explorerUrl: string; - displayName: string; -}; -``` - -Wallet, transaction, signature, and indexer APIs take `chainId` as a string. - -## Signing - -```ts -signMessage(chainId: string, message: string): Promise - -signTypedData({ +oms.wallet.signTypedData({ chainId: string; typedData: unknown; }): Promise -verifyMessageSignature({ +oms.wallet.verifyMessageSignature({ chainId: string; message: string; signature: string; }): Promise -verifyTypedDataSignature({ +oms.wallet.verifyTypedDataSignature({ chainId: string; typedData: unknown; signature: string; @@ -255,10 +219,10 @@ verifyTypedDataSignature({ Signature verification checks against the active wallet session. -## Transactions +### Transactions ```ts -sendTransaction({ +oms.wallet.sendTransaction({ chainId: string; to: string; value: string; @@ -269,7 +233,7 @@ sendTransaction({ statusPolling?: OmsTransactionStatusPollingOptions; }): Promise -callContract({ +oms.wallet.callContract({ chainId: string; contractAddress: string; method: string; @@ -280,7 +244,7 @@ callContract({ statusPolling?: OmsTransactionStatusPollingOptions; }): Promise -getTransactionStatus(txnId: string): Promise +oms.wallet.getTransactionStatus(txnId: string): Promise ``` ```ts @@ -306,12 +270,7 @@ type OmsTransactionStatusPollingOptions = { `value` is a raw base-unit integer string. Use `parseUnits` for display-value conversion before sending. -By default, transaction methods poll WaaS after execute until status resolves -or the default timeout is reached. Pass `waitForStatus: false` to return -immediately after execute, or pass `statusPolling` to tune the post-execute -polling timeout, normal interval, fast interval, and fast-poll count. - -## Fee Selection +### Fee Selection ```ts type OmsFeeOptionSelector = ( @@ -332,170 +291,160 @@ type OmsFeeOptionWithBalance = { }; ``` -Return `option.selection` when choosing a quoted fee option. It uses `tokenId` -when present and falls back to the token symbol for native fee options. -Returning `null` is only valid for sponsored transactions. +Return `option.selection` when choosing a quoted fee option. -## Balances +### ID Token And Access ```ts -getTokenBalances({ - chainId: string; - contractAddress?: string; +oms.wallet.getIdToken({ + ttlSeconds?: number | null; + customClaims?: Record | null; +} = {}): Promise + +oms.wallet.listAccess({ pageSize?: number | null } = {}): Promise + +oms.wallet.listAccessPages( + { pageSize?: number | null } = {} +): AsyncGenerator + +oms.wallet.listAccessPage({ + pageSize?: number | null; + cursor?: string | null; +} = {}): Promise + +oms.wallet.revokeAccess(targetCredentialId: string): Promise +``` + +## Indexer + +All indexer APIs are accessed through `oms.indexer`. + +```ts +oms.indexer.getBalances({ walletAddress: string; + networks?: OmsNetwork[]; + networkType?: 'MAINNETS' | 'TESTNETS' | 'ALL'; + contractAddresses?: string[]; includeMetadata?: boolean; + omitPrices?: boolean | null; + tokenIds?: string[]; + contractStatus?: 'VERIFIED' | 'UNVERIFIED' | 'ALL' | null; page?: { page?: number; pageSize?: number; }; -}): Promise +}): Promise -getNativeTokenBalance({ - chainId: string; +oms.indexer.getTransactionHistory({ walletAddress: string; -}): Promise + networks?: OmsNetwork[]; + networkType?: 'MAINNETS' | 'TESTNETS' | 'ALL'; + contractAddresses?: string[]; + transactionHashes?: string[]; + metaTransactionIds?: string[]; + fromBlock?: number | null; + toBlock?: number | null; + tokenId?: string | null; + includeMetadata?: boolean; + omitPrices?: boolean | null; + metadataOptions?: { + verifiedOnly?: boolean; + unverifiedOnly?: boolean; + includeContracts?: string[]; + } | null; + page?: { + page?: number; + pageSize?: number; + }; +}): Promise ``` ```ts -type OmsTokenBalance = { - contractType: string | null; - contractAddress: string | null; - accountAddress: string | null; - tokenId: string | null; - balance: string | null; - balanceUSD?: string | null; - priceUSD?: string | null; - priceUpdatedAt?: string | null; - blockHash: string | null; - blockNumber?: number | null; - chainId?: number | null; - uniqueCollectibles?: string | null; - isSummary?: boolean | null; - contractInfo?: OmsTokenContractInfo | null; - tokenMetadata?: OmsTokenMetadata | null; +type OmsBalancesResult = { + status: number; + page?: OmsTokenBalancesPage | null; + nativeBalances: OmsTokenBalance[]; + balances: OmsTokenBalance[]; }; -type OmsTokenBalancesPage = { - page: number; - pageSize: number; - more: boolean; +type OmsTransactionHistoryResult = { + status: number; + page?: OmsTokenBalancesPage | null; + transactions: OmsTransaction[]; }; -type OmsTokenContractInfo = { - chainId?: number | null; - address?: string | null; - source?: string | null; - name?: string | null; - type?: string | null; - symbol?: string | null; - decimals?: number | null; - logoURI?: string | null; - deployed?: boolean | null; - bytecodeHash?: string | null; - extensions?: object | null; - updatedAt?: string | null; - queuedAt?: string | null; - status?: string | null; -}; - -type OmsTokenMetadata = { - chainId?: number | null; - contractAddress?: string | null; - tokenId?: string | null; - source?: string | null; - name?: string | null; - description?: string | null; - image?: string | null; - video?: string | null; - audio?: string | null; - properties?: object | null; - attributes?: object[] | null; - imageData?: string | null; - externalUrl?: string | null; - backgroundColor?: string | null; - animationUrl?: string | null; - decimals?: number | null; - updatedAt?: string | null; - assets?: OmsTokenMetadataAsset[] | null; - status?: string | null; - queuedAt?: string | null; - lastFetched?: string | null; -}; - -type OmsTokenMetadataAsset = { - id?: number | null; - collectionId?: number | null; - tokenId?: string | null; - url?: string | null; - metadataField?: string | null; - name?: string | null; - filesize?: number | null; - mimeType?: string | null; - width?: number | null; - height?: number | null; - updatedAt?: string | null; +type OmsTransaction = { + txnHash: string | null; + blockNumber: number | null; + blockHash: string | null; + chainId: number | null; + metaTxnId?: string | null; + transfers?: OmsTransactionTransfer[] | null; + timestamp?: string | null; }; ``` -Omit `contractAddress` to query balances across token contracts. Pass `page` -to request a later page or a custom page size. When `page` is undefined, the -request defaults to page `0` with up to `40` entries. -Pass `includeMetadata: true` to request `contractInfo` and `tokenMetadata`. +Pass `networks` for explicit chain selection. If omitted, the native SDK uses +`networkType`, which defaults to `MAINNETS`. -## Wallet ID Token +## Networks ```ts -getIdToken({ - ttlSeconds?: number | null; - customClaims?: Record | null; -}): Promise +oms.supportedNetworks: OmsNetwork[] ``` -## Wallet Access - ```ts -listAccess({ pageSize?: number | null } = {}): Promise - -listAccessPages( - { pageSize?: number | null } = {} -): AsyncGenerator +type OmsNetwork = { + chainId: string; + name: string; + nativeTokenSymbol: string; + explorerUrl: string; + displayName: string; +}; +``` -listAccessPage({ - pageSize?: number | null; - cursor?: string | null; -} = {}): Promise +Wallet, transaction, signature, and indexer APIs take `chainId` as a string. -revokeAccess(targetCredentialId: string): Promise -``` +## Token Types ```ts -type OmsCredentialInfo = { - credentialId: string; - expiresAt: string; - isCaller: boolean; +type OmsTokenBalance = { + contractType: string | null; + contractAddress: string | null; + accountAddress: string | null; + tokenId: string | null; + balance: string | null; + blockHash: string | null; + blockNumber?: number | null; + chainId?: number | null; + name?: string | null; + symbol?: string | null; + balanceUSD?: string | null; + priceUSD?: string | null; + priceUpdatedAt?: string | null; + uniqueCollectibles?: string | null; + isSummary?: boolean | null; + contractInfo?: OmsTokenContractInfo | null; + tokenMetadata?: OmsTokenMetadata | null; }; -type OmsListAccessResponse = { - credentials: OmsCredentialInfo[]; - page: { - limit: number | null; - cursor: string | null; - } | null; +type OmsTokenBalancesPage = { + page: number | null; + pageSize: number | null; + more: boolean | null; }; ``` -## Unit Helpers +`OmsTokenContractInfo`, `OmsTokenMetadata`, and `OmsTokenMetadataAsset` mirror +the native SDK token metadata objects. -```ts -parseUnits( - value: string, - decimals?: number, - options?: { roundingMode?: 'reject' | 'nearest' } -): string +## Formatting Helpers -formatUnits(value: string | bigint, decimals?: number): string +```ts +parseUnits(value: string, decimals?: number, options?: ParseUnitsOptions): string +formatUnits(value: string, decimals?: number): string ``` -`parseUnits` defaults to `roundingMode: 'nearest'`, matching the native SDK -helpers by rounding fractional precision beyond `decimals` to the nearest base -unit. Use `roundingMode: 'reject'` to fail on non-zero excess precision. +By default `parseUnits` rounds fractional precision beyond `decimals` to the +nearest base unit. Pass `{ roundingMode: 'reject' }` to fail on non-zero excess +precision. diff --git a/OmsClientReactNativeSdk.podspec b/OmsClientReactNativeSdk.podspec index 44931bc..2b3858b 100644 --- a/OmsClientReactNativeSdk.podspec +++ b/OmsClientReactNativeSdk.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift,cpp}" s.private_header_files = "ios/**/*.h" s.swift_version = "6.0" - s.dependency "oms-client-swift-sdk", "0.1.0-alpha.2" + s.dependency "oms-client-swift-sdk", "0.1.0-alpha.3" s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" } diff --git a/README.md b/README.md index e6d0883..141bf7a 100644 --- a/README.md +++ b/README.md @@ -12,26 +12,18 @@ npm install @0xsequence/oms-react-native-sdk ```ts import { - completeEmailAuth, - configure, - formatUnits, - getWalletAddress, - handleOidcRedirectCallback, + OMSClient, OidcProviders, + formatUnits, parseUnits, - sendTransaction, - signMessage, - startEmailAuth, - startOidcRedirectAuth, } from '@0xsequence/oms-react-native-sdk'; -await configure({ +const oms = new OMSClient({ publishableKey: '', - projectId: '', }); -await startEmailAuth('player@example.com'); -const auth = await completeEmailAuth({ code: '' }); +await oms.wallet.startEmailAuth('player@example.com'); +const auth = await oms.wallet.completeEmailAuth({ code: '' }); if (auth.type === 'walletSelection') { const selected = await auth.pendingSelection.selectWallet(''); @@ -40,21 +32,23 @@ if (auth.type === 'walletSelection') { console.log(auth.walletAddress); } -const signature = await signMessage('137', 'Hello from React Native'); -const address = await getWalletAddress(); +const signature = await oms.wallet.signMessage( + '137', + 'Hello from React Native' +); +const address = await oms.wallet.getWalletAddress(); ``` ### OIDC Redirect Auth -The SDK exposes the low-level redirect methods. Apps own browser opening and -deep-link handling. Use a system auth browser such as Custom Tabs or -ASWebAuthenticationSession/SFAuthenticationSession; do not run provider OAuth in -an embedded WebView. +Apps own browser opening and deep-link handling. Use a system auth browser such +as Custom Tabs or ASWebAuthenticationSession/SFAuthenticationSession; do not run +provider OAuth in an embedded WebView. ```ts import { InAppBrowser } from 'react-native-inappbrowser-reborn'; -const started = await startOidcRedirectAuth({ +const started = await oms.wallet.startOidcRedirectAuth({ provider: OidcProviders.google(), redirectUri: 'com.example.app:/oauth/callback', }); @@ -68,7 +62,7 @@ if (browserResult.type !== 'success') { throw new Error('OIDC sign-in was cancelled'); } -const result = await handleOidcRedirectCallback({ +const result = await oms.wallet.handleOidcRedirectCallback({ callbackUrl: browserResult.url, walletSelection: 'manual', }); @@ -78,10 +72,32 @@ if (result.type === 'walletSelection') { } ``` +### Indexer + +```ts +const polygon = oms.supportedNetworks.find( + (network) => network.chainId === '137' +); + +const balances = await oms.indexer.getBalances({ + walletAddress: address!, + networks: polygon ? [polygon] : undefined, + includeMetadata: true, +}); + +const history = await oms.indexer.getTransactionHistory({ + walletAddress: address!, + networks: polygon ? [polygon] : undefined, +}); +``` + +`getBalances` returns `nativeBalances` separately from token-contract +`balances`. + ### Fee Option Selection ```ts -const txResult = await sendTransaction({ +const txResult = await oms.wallet.sendTransaction({ chainId: '137', to: '0xRecipient', value: '0', @@ -95,10 +111,7 @@ const txResult = await sendTransaction({ `selectFeeOption` receives the same enriched fee options as the native SDKs: `feeOption`, wallet `balance`, formatted `available`, raw `availableRaw`, and -`decimals`. Return `option.selection` for a quoted option; it preserves token IDs -when present and falls back to the token symbol for native fee options. Returning -`null` means no fee option is selected, which is only valid for sponsored -transactions. +`decimals`. Return `option.selection` for a quoted option. ### Unit Formatting @@ -118,15 +131,14 @@ See [API.md](./API.md) for the public API surface and TypeScript shapes. ## Supported APIs -- Email OTP auth and OIDC ID-token auth -- Manual wallet selection for email, OIDC ID-token, and OIDC redirect auth -- Low-level OIDC redirect auth start/callback handling +- Email OTP auth, OIDC ID-token auth, and OIDC redirect auth +- Manual wallet selection - Session restore, sign-out, wallet address, and session metadata - Wallet list, use existing wallet, and create wallet -- Supported network listing +- Synchronous supported network list via `oms.supportedNetworks` - Message and typed-data signing and verification - Transaction sending, custom fee-option selection, contract calls, and transaction status lookup -- Token balances and native token balance +- Indexer balances and transaction history - Wallet ID token retrieval - Wallet access list, access-page iteration, single-page access lookup, and revoke access - Unit parsing and formatting helpers @@ -134,8 +146,8 @@ See [API.md](./API.md) for the public API surface and TypeScript shapes. ## Native SDK Dependencies The React Native SDK owns its native SDK dependencies. Android resolves -`io.github.0xsequence:oms-client-kotlin-sdk:0.1.0-alpha.2` from Maven, and -iOS resolves `oms-client-swift-sdk` `0.1.0-alpha.2` from CocoaPods. +`io.github.0xsequence:oms-client-kotlin-sdk:0.1.0-alpha.3` from Maven, and iOS +resolves `oms-client-swift-sdk` `0.1.0-alpha.3` from CocoaPods. The React Native wrapper itself is distributed through npm. React Native autolinking consumes the wrapper podspec and Android project from @@ -163,7 +175,7 @@ on the underlying native SDKs. - `examples/expo-example` is a standalone Expo development-build demo that uses `expo-web-browser` and the published npm package. It is intentionally excluded from the root Yarn workspace so it is not linked to the local SDK - source. + source. Its dependency is not updated until this SDK version is published. ## Publishing diff --git a/android/build.gradle b/android/build.gradle index 431cdcc..da3664c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,11 +1,12 @@ buildscript { ext.OmsClientReactNativeSdk = [ - kotlinVersion: "2.2.10", + kotlinVersion: "2.3.20", minSdkVersion: 26, compileSdkVersion: 36, targetSdkVersion: 36, - omsClientKotlinSdkVersion: "0.1.0-alpha.2", - kotlinxSerializationJsonVersion: "1.9.0" + omsClientKotlinSdkVersion: "0.1.0-alpha.3", + kotlinxCoroutinesVersion: "1.11.0", + kotlinxSerializationJsonVersion: "1.11.0" ] ext.getExtOrDefault = { prop -> @@ -78,6 +79,6 @@ repositories { dependencies { implementation "com.facebook.react:react-android" implementation "io.github.0xsequence:oms-client-kotlin-sdk:${getExtOrDefault('omsClientKotlinSdkVersion')}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${getExtOrDefault('kotlinxCoroutinesVersion')}" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:${getExtOrDefault('kotlinxSerializationJsonVersion')}" } diff --git a/android/src/main/java/com/omsclientreactnativesdk/OmsClientReactNativeSdkModule.kt b/android/src/main/java/com/omsclientreactnativesdk/OmsClientReactNativeSdkModule.kt index 0bf4464..1ce8603 100644 --- a/android/src/main/java/com/omsclientreactnativesdk/OmsClientReactNativeSdkModule.kt +++ b/android/src/main/java/com/omsclientreactnativesdk/OmsClientReactNativeSdkModule.kt @@ -10,14 +10,18 @@ import com.omsclient.kotlin_sdk.OMSClient import com.omsclient.kotlin_sdk.OMSClientSessionState import com.omsclient.kotlin_sdk.OmsSdkErrorCode import com.omsclient.kotlin_sdk.OmsSdkException +import com.omsclient.kotlin_sdk.OmsUpstreamError import com.omsclient.kotlin_sdk.models.AbiArg +import com.omsclient.kotlin_sdk.models.ContractVerificationStatus import com.omsclient.kotlin_sdk.models.CredentialInfo import com.omsclient.kotlin_sdk.models.FeeOption import com.omsclient.kotlin_sdk.models.FeeOptionSelection import com.omsclient.kotlin_sdk.models.FeeOptionSelector import com.omsclient.kotlin_sdk.models.FeeOptionWithBalance import com.omsclient.kotlin_sdk.models.FeeToken +import com.omsclient.kotlin_sdk.models.IndexerNetworkType import com.omsclient.kotlin_sdk.models.ListAccessResponse +import com.omsclient.kotlin_sdk.models.MetadataOptions import com.omsclient.kotlin_sdk.models.Page import com.omsclient.kotlin_sdk.models.SendTransactionRequest import com.omsclient.kotlin_sdk.models.TokenBalance @@ -27,12 +31,14 @@ import com.omsclient.kotlin_sdk.models.TokenBalancesResult import com.omsclient.kotlin_sdk.models.TokenContractInfo import com.omsclient.kotlin_sdk.models.TokenMetadata import com.omsclient.kotlin_sdk.models.TokenMetadataAsset +import com.omsclient.kotlin_sdk.models.Transaction +import com.omsclient.kotlin_sdk.models.TransactionHistoryResult import com.omsclient.kotlin_sdk.models.TransactionMode import com.omsclient.kotlin_sdk.models.TransactionStatusPollingOptions import com.omsclient.kotlin_sdk.models.TransactionStatusResponse +import com.omsclient.kotlin_sdk.models.TransactionTransfer import com.omsclient.kotlin_sdk.models.Wallet import com.omsclient.kotlin_sdk.models.WalletType -import com.omsclient.kotlin_sdk.network.OMSClientEnvironment import com.omsclient.kotlin_sdk.wallet.CompleteAuthResult import com.omsclient.kotlin_sdk.wallet.OidcProviderConfig import com.omsclient.kotlin_sdk.wallet.OidcRedirectAuthResult @@ -61,38 +67,36 @@ import java.math.BigInteger import java.util.UUID import java.util.concurrent.ConcurrentHashMap +private data class StoredPendingWalletSelection( + val clientId: String, + val selection: PendingWalletSelection +) + class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : NativeOmsClientReactNativeSdkSpec(reactContext) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val pendingFeeOptionSelections = ConcurrentHashMap>() - private val pendingWalletSelections = ConcurrentHashMap() - private var sessionExpiredUnsubscribe: (() -> Unit)? = null - private var client: OMSClient? = null + private val pendingWalletSelections = ConcurrentHashMap() + private val sessionExpiredUnsubscribes = ConcurrentHashMap Unit>() + private val clients = ConcurrentHashMap() - override fun configure( + override fun createClient( + clientId: String, publishableKey: String, - walletApiUrl: String?, - apiRpcUrl: String?, - indexerUrlTemplate: String?, - projectId: String, promise: Promise ) { try { - pendingWalletSelections.clear() - sessionExpiredUnsubscribe?.invoke() - client = OMSClient( + clearPendingWalletSelections(clientId) + sessionExpiredUnsubscribes.remove(clientId)?.invoke() + val activeClient = OMSClient( context = reactApplicationContext, - publishableKey = publishableKey, - projectId = projectId, - environment = OMSClientEnvironment( - walletApiUrl ?: OMSClientEnvironment.walletApiUrlDefault, - apiRpcUrl ?: OMSClientEnvironment.apiRpcUrlDefault, - indexerUrlTemplate ?: OMSClientEnvironment.indexerUrlTemplateDefault - ) + publishableKey = publishableKey ) - sessionExpiredUnsubscribe = client?.wallet?.onSessionExpired { event -> + clients[clientId] = activeClient + sessionExpiredUnsubscribes[clientId] = activeClient.wallet.onSessionExpired { event -> emitOnSessionExpired( Arguments.createMap().apply { + putString("clientId", clientId) putMap("session", sessionMap(event.session)) putString("expiredAt", event.expiredAt.toString()) } @@ -104,38 +108,31 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } } - override fun getWalletAddress(promise: Promise) { - promise.resolve(client?.session?.walletAddress) - } - - override fun getSession(promise: Promise) { - promise.resolve(sessionMap(client?.session)) + override fun getWalletAddress(clientId: String, promise: Promise) { + try { + promise.resolve(requireClient(clientId).session.walletAddress) + } catch (throwable: Throwable) { + reject(promise, throwable) + } } - override fun getSupportedNetworks(promise: Promise) { - val networks = Arguments.createArray() - Network.entries.forEach { network -> - networks.pushMap( - Arguments.createMap().apply { - putString("chainId", network.id.toString()) - putString("name", network.name) - putString("nativeTokenSymbol", network.nativeTokenSymbol) - putString("explorerUrl", network.explorerUrl) - putString("displayName", network.displayName) - } - ) + override fun getSession(clientId: String, promise: Promise) { + try { + promise.resolve(sessionMap(requireClient(clientId).session)) + } catch (throwable: Throwable) { + reject(promise, throwable) } - promise.resolve(networks) } - override fun startEmailAuth(email: String, promise: Promise) { + override fun startEmailAuth(clientId: String, email: String, promise: Promise) { launch(promise) { - requireClient().wallet.startEmailAuth(email) + requireClient(clientId).wallet.startEmailAuth(email) null } } override fun completeEmailAuth( + clientId: String, code: String, walletSelection: String?, walletType: String?, @@ -144,7 +141,8 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : ) { launch(promise) { completeAuthResultMap( - requireClient().wallet.completeEmailAuth( + clientId, + requireClient(clientId).wallet.completeEmailAuth( code = code, walletSelection = walletSelection.toWalletSelectionBehavior(), walletType = walletType.toWalletType(), @@ -155,6 +153,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } override fun signInWithOidcIdToken( + clientId: String, idToken: String, issuer: String, audience: String, @@ -165,7 +164,8 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : ) { launch(promise) { completeAuthResultMap( - requireClient().wallet.signInWithOidcIdToken( + clientId, + requireClient(clientId).wallet.signInWithOidcIdToken( idToken = idToken, issuer = issuer, audience = audience, @@ -178,6 +178,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } override fun startOidcRedirectAuth( + clientId: String, providerJson: String, redirectUri: String, walletType: String?, @@ -187,7 +188,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : promise: Promise ) { launch(promise) { - val result = requireClient().wallet.startOidcRedirectAuth( + val result = requireClient(clientId).wallet.startOidcRedirectAuth( provider = providerJson.toOidcProviderConfig(), redirectUri = redirectUri, walletType = walletType.toWalletType(), @@ -204,6 +205,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } override fun handleOidcRedirectCallback( + clientId: String, callbackUrl: String?, walletSelection: String?, sessionLifetimeSeconds: String?, @@ -211,7 +213,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : ) { launch(promise) { when ( - val result = requireClient().wallet.handleOidcRedirectCallback( + val result = requireClient(clientId).wallet.handleOidcRedirectCallback( callbackUrl = callbackUrl, walletSelection = walletSelection.toWalletSelectionBehavior(), sessionLifetimeSeconds = sessionLifetimeSeconds.toSessionLifetimeSeconds() @@ -219,7 +221,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : ) { is OidcRedirectAuthResult.Completed -> Arguments.createMap().apply { - pendingWalletSelections.clear() + clearPendingWalletSelections(clientId) putString("type", "completed") putMap("wallet", walletMap(result.wallet)) } @@ -238,32 +240,32 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : is OidcRedirectAuthResult.WalletSelection -> Arguments.createMap().apply { - pendingWalletSelections.clear() + clearPendingWalletSelections(clientId) putString("type", "walletSelection") - putMap("pendingSelection", pendingWalletSelectionMap(result.pendingSelection)) + putMap("pendingSelection", pendingWalletSelectionMap(clientId, result.pendingSelection)) } } } } - override fun listWallets(promise: Promise) { + override fun listWallets(clientId: String, promise: Promise) { launch(promise) { Arguments.createArray().apply { - requireClient().wallet.listWallets().forEach { pushMap(walletMap(it)) } + requireClient(clientId).wallet.listWallets().forEach { pushMap(walletMap(it)) } } } } - override fun useWallet(walletId: String, promise: Promise) { + override fun useWallet(clientId: String, walletId: String, promise: Promise) { launch(promise) { - walletActivationResultMap(requireClient().wallet.useWallet(walletId)) + walletActivationResultMap(requireClient(clientId).wallet.useWallet(walletId)) } } - override fun createWallet(walletType: String?, reference: String?, promise: Promise) { + override fun createWallet(clientId: String, walletType: String?, reference: String?, promise: Promise) { launch(promise) { walletActivationResultMap( - requireClient().wallet.createWallet( + requireClient(clientId).wallet.createWallet( walletType = walletType.toWalletType(), reference = reference ) @@ -272,6 +274,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } override fun selectWalletForPendingSelection( + clientId: String, pendingSelectionId: String, walletId: String, promise: Promise @@ -280,13 +283,17 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : val pendingSelection = pendingWalletSelections[pendingSelectionId] ?: error("Pending wallet selection is no longer available") - val result = pendingSelection.selectWallet(walletId) + if (pendingSelection.clientId != clientId) { + error("Pending wallet selection belongs to a different OMS client") + } + val result = pendingSelection.selection.selectWallet(walletId) pendingWalletSelections.remove(pendingSelectionId) walletActivationResultMap(result) } } override fun createAndSelectWalletForPendingSelection( + clientId: String, pendingSelectionId: String, reference: String?, promise: Promise @@ -295,25 +302,28 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : val pendingSelection = pendingWalletSelections[pendingSelectionId] ?: error("Pending wallet selection is no longer available") - val result = pendingSelection.createAndSelectWallet(reference) + if (pendingSelection.clientId != clientId) { + error("Pending wallet selection belongs to a different OMS client") + } + val result = pendingSelection.selection.createAndSelectWallet(reference) pendingWalletSelections.remove(pendingSelectionId) walletActivationResultMap(result) } } - override fun signOut(promise: Promise) { + override fun signOut(clientId: String, promise: Promise) { try { - pendingWalletSelections.clear() - requireClient().wallet.signOut() + clearPendingWalletSelections(clientId) + requireClient(clientId).wallet.signOut() promise.resolve(null) } catch (throwable: Throwable) { reject(promise, throwable) } } - override fun signMessage(chainId: String, message: String, promise: Promise) { + override fun signMessage(clientId: String, chainId: String, message: String, promise: Promise) { launch(promise) { - val activeClient = requireClient() + val activeClient = requireClient(clientId) activeClient.wallet.signMessage( network = activeClient.requireNetwork(chainId), message = message @@ -321,9 +331,9 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } } - override fun signTypedData(chainId: String, typedDataJson: String, promise: Promise) { + override fun signTypedData(clientId: String, chainId: String, typedDataJson: String, promise: Promise) { launch(promise) { - val activeClient = requireClient() + val activeClient = requireClient(clientId) activeClient.wallet.signTypedData( network = activeClient.requireNetwork(chainId), typedData = json.parseToJsonElement(typedDataJson) @@ -332,6 +342,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } override fun sendTransaction( + clientId: String, chainId: String, to: String, value: String, @@ -346,7 +357,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : promise: Promise ) { launch(promise) { - val activeClient = requireClient() + val activeClient = requireClient(clientId) val result = activeClient.wallet.sendTransaction( network = activeClient.requireNetwork(chainId), request = SendTransactionRequest( @@ -370,6 +381,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } override fun callContract( + clientId: String, chainId: String, contractAddress: String, method: String, @@ -384,7 +396,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : promise: Promise ) { launch(promise) { - val activeClient = requireClient() + val activeClient = requireClient(clientId) val result = activeClient.wallet.callContract( network = activeClient.requireNetwork(chainId), contract = contractAddress, @@ -427,56 +439,73 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } } - override fun getTransactionStatus(txnId: String, promise: Promise) { + override fun getTransactionStatus(clientId: String, txnId: String, promise: Promise) { launch(promise) { - transactionStatusMap(requireClient().wallet.getTransactionStatus(txnId)) + transactionStatusMap(requireClient(clientId).wallet.getTransactionStatus(txnId)) } } - override fun getTokenBalances( - chainId: String, - contractAddress: String?, - walletAddress: String, - includeMetadata: Boolean, - page: String?, - pageSize: String?, + override fun getBalances( + clientId: String, + paramsJson: String, promise: Promise ) { launch(promise) { - val activeClient = requireClient() + val activeClient = requireClient(clientId) + val params = paramsJson.toJsonObject("params") tokenBalancesResultMap( - activeClient.indexer.getTokenBalances( - network = activeClient.requireNetwork(chainId), - contractAddress = contractAddress, - walletAddress = walletAddress, - includeMetadata = includeMetadata, - page = TokenBalancesPageRequest( - page = page.toIntOrNullParam("page") ?: 0, - pageSize = pageSize.toIntOrNullParam("pageSize") ?: 40 - ) + activeClient.indexer.getBalances( + walletAddress = params.requiredStringParam("walletAddress"), + networks = params.networksParam(activeClient), + networkType = params.indexerNetworkTypeParam("networkType") ?: IndexerNetworkType.MAINNETS, + contractAddresses = params.stringListParam("contractAddresses"), + includeMetadata = params.booleanParam("includeMetadata") ?: true, + omitPrices = params.booleanParam("omitPrices"), + tokenIds = params.stringListParam("tokenIds"), + contractStatus = params.contractVerificationStatusParam("contractStatus"), + page = params.tokenBalancesPageRequestParam() ) ) } } - override fun getNativeTokenBalance(chainId: String, walletAddress: String, promise: Promise) { + override fun getTransactionHistory( + clientId: String, + paramsJson: String, + promise: Promise + ) { launch(promise) { - val activeClient = requireClient() - activeClient.indexer.getNativeTokenBalance( - network = activeClient.requireNetwork(chainId), - walletAddress = walletAddress - )?.let(::tokenBalanceMap) + val activeClient = requireClient(clientId) + val params = paramsJson.toJsonObject("params") + transactionHistoryResultMap( + activeClient.indexer.getTransactionHistory( + walletAddress = params.requiredStringParam("walletAddress"), + networks = params.networksParam(activeClient), + networkType = params.indexerNetworkTypeParam("networkType") ?: IndexerNetworkType.MAINNETS, + contractAddresses = params.stringListParam("contractAddresses"), + transactionHashes = params.stringListParam("transactionHashes"), + metaTransactionIds = params.stringListParam("metaTransactionIds"), + fromBlock = params.longParam("fromBlock"), + toBlock = params.longParam("toBlock"), + tokenId = params.stringParam("tokenId"), + includeMetadata = params.booleanParam("includeMetadata") ?: true, + omitPrices = params.booleanParam("omitPrices"), + metadataOptions = params.metadataOptionsParam("metadataOptions"), + page = params.tokenBalancesPageRequestParam() + ) + ) } } override fun verifyMessageSignature( + clientId: String, chainId: String, message: String, signature: String, promise: Promise ) { launch(promise) { - val activeClient = requireClient() + val activeClient = requireClient(clientId) activeClient.wallet.isValidMessageSignature( network = activeClient.requireNetwork(chainId), message = message, @@ -486,13 +515,14 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } override fun verifyTypedDataSignature( + clientId: String, chainId: String, typedDataJson: String, signature: String, promise: Promise ) { launch(promise) { - val activeClient = requireClient() + val activeClient = requireClient(clientId) activeClient.wallet.isValidTypedDataSignature( network = activeClient.requireNetwork(chainId), typedData = json.parseToJsonElement(typedDataJson), @@ -501,29 +531,29 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } } - override fun getIdToken(ttlSeconds: String?, customClaimsJson: String?, promise: Promise) { + override fun getIdToken(clientId: String, ttlSeconds: String?, customClaimsJson: String?, promise: Promise) { launch(promise) { - requireClient().wallet.getIdToken( + requireClient(clientId).wallet.getIdToken( ttlSeconds = ttlSeconds.toUIntOrNullParam("ttlSeconds"), customClaims = customClaimsJson.toJsonObjectMap("customClaims") ) } } - override fun listAccess(pageSize: String?, promise: Promise) { + override fun listAccess(clientId: String, pageSize: String?, promise: Promise) { launch(promise) { Arguments.createArray().apply { - requireClient().wallet.listAccess( + requireClient(clientId).wallet.listAccess( pageSize = pageSize.toUIntOrNullParam("pageSize") ).forEach { pushMap(credentialInfoMap(it)) } } } } - override fun listAccessPage(pageSize: String?, cursor: String?, promise: Promise) { + override fun listAccessPage(clientId: String, pageSize: String?, cursor: String?, promise: Promise) { launch(promise) { listAccessResponseMap( - requireClient().wallet.listAccessPage( + requireClient(clientId).wallet.listAccessPage( pageSize = pageSize.toUIntOrNullParam("pageSize"), cursor = cursor ) @@ -531,16 +561,18 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } } - override fun revokeAccess(targetCredentialId: String, promise: Promise) { + override fun revokeAccess(clientId: String, targetCredentialId: String, promise: Promise) { launch(promise) { - requireClient().wallet.revokeAccess(targetCredentialId) + requireClient(clientId).wallet.revokeAccess(targetCredentialId) null } } override fun invalidate() { - sessionExpiredUnsubscribe?.invoke() - sessionExpiredUnsubscribe = null + sessionExpiredUnsubscribes.values.forEach { it.invoke() } + sessionExpiredUnsubscribes.clear() + clients.clear() + pendingWalletSelections.clear() scope.cancel() super.invalidate() } @@ -557,16 +589,16 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } } - private fun requireClient(): OMSClient = - client ?: error("Call configure before using the OMS client") + private fun requireClient(clientId: String): OMSClient = + clients[clientId] ?: error("OMS client is not initialized: $clientId") private fun OMSClient.requireNetwork(chainId: String): Network = supportedNetworks.firstOrNull { it.id.toString() == chainId } ?: error("Unsupported chain id: $chainId") - private fun completeAuthResultMap(result: CompleteAuthResult): WritableMap = + private fun completeAuthResultMap(clientId: String, result: CompleteAuthResult): WritableMap = when (result) { is CompleteAuthResult.WalletSelected -> { - pendingWalletSelections.clear() + clearPendingWalletSelections(clientId) Arguments.createMap().apply { putString("type", "walletSelected") putString("walletAddress", result.walletAddress) @@ -583,7 +615,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : is CompleteAuthResult.WalletSelection -> Arguments.createMap().apply { - pendingWalletSelections.clear() + clearPendingWalletSelections(clientId) putString("type", "walletSelection") putNull("walletAddress") putNull("wallet") @@ -594,7 +626,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } ) putMap("credential", credentialInfoMap(result.pendingSelection.credential)) - putMap("pendingSelection", pendingWalletSelectionMap(result.pendingSelection)) + putMap("pendingSelection", pendingWalletSelectionMap(clientId, result.pendingSelection)) } } @@ -643,9 +675,12 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : putNullableString("reference", wallet.reference) } - private fun pendingWalletSelectionMap(pendingSelection: PendingWalletSelection): WritableMap { + private fun pendingWalletSelectionMap( + clientId: String, + pendingSelection: PendingWalletSelection + ): WritableMap { val id = UUID.randomUUID().toString() - pendingWalletSelections[id] = pendingSelection + pendingWalletSelections[id] = StoredPendingWalletSelection(clientId, pendingSelection) return Arguments.createMap().apply { putString("id", id) putString("walletType", pendingSelection.walletType.wireValue) @@ -659,6 +694,10 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : } } + private fun clearPendingWalletSelections(clientId: String) { + pendingWalletSelections.entries.removeIf { it.value.clientId == clientId } + } + private fun walletActivationResultMap(result: WalletSelectionResult): WritableMap = Arguments.createMap().apply { putString("walletAddress", result.walletAddress) @@ -676,7 +715,13 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : private fun tokenBalancesResultMap(result: TokenBalancesResult): WritableMap = Arguments.createMap().apply { putInt("status", result.status) - result.page?.let { putMap("page", tokenBalancesPageMap(it)) } + result.page?.let { putMap("page", tokenBalancesPageMap(it)) } ?: putNull("page") + putArray( + "nativeBalances", + Arguments.createArray().apply { + result.nativeBalances.forEach { pushMap(tokenBalanceMap(it)) } + } + ) putArray( "balances", Arguments.createArray().apply { @@ -685,6 +730,18 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : ) } + private fun transactionHistoryResultMap(result: TransactionHistoryResult): WritableMap = + Arguments.createMap().apply { + putInt("status", result.status) + result.page?.let { putMap("page", tokenBalancesPageMap(it)) } ?: putNull("page") + putArray( + "transactions", + Arguments.createArray().apply { + result.transactions.forEach { pushMap(transactionMap(it)) } + } + ) + } + private fun tokenBalancesPageMap(page: TokenBalancesPage): WritableMap = Arguments.createMap().apply { putInt("page", page.page) @@ -699,6 +756,8 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : putNullableString("accountAddress", balance.accountAddress) putNullableString("tokenId", balance.tokenId) putNullableString("balance", balance.balance) + putNullableString("name", balance.name) + putNullableString("symbol", balance.symbol) putNullableString("balanceUSD", balance.balanceUSD) putNullableString("priceUSD", balance.priceUSD) putNullableString("priceUpdatedAt", balance.priceUpdatedAt) @@ -783,6 +842,50 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : putNullableString("updatedAt", asset.updatedAt) } + private fun transactionMap(transaction: Transaction): WritableMap = + Arguments.createMap().apply { + putNullableString("txnHash", transaction.txnHash) + transaction.blockNumber?.let { putDouble("blockNumber", it.toDouble()) } ?: putNull("blockNumber") + putNullableString("blockHash", transaction.blockHash) + transaction.chainId?.let { putDouble("chainId", it.toDouble()) } ?: putNull("chainId") + putNullableString("metaTxnId", transaction.metaTxnId) + transaction.transfers?.let { + putArray( + "transfers", + Arguments.createArray().apply { + it.forEach { transfer -> pushMap(transactionTransferMap(transfer)) } + } + ) + } ?: putNull("transfers") + putNullableString("timestamp", transaction.timestamp) + } + + private fun transactionTransferMap(transfer: TransactionTransfer): WritableMap = + Arguments.createMap().apply { + putNullableString("transferType", transfer.transferType) + putNullableString("contractAddress", transfer.contractAddress) + putNullableString("contractType", transfer.contractType) + putNullableString("from", transfer.from) + putNullableString("to", transfer.to) + transfer.tokenIds?.let { putArray("tokenIds", stringArray(it)) } ?: putNull("tokenIds") + transfer.amounts?.let { putArray("amounts", stringArray(it)) } ?: putNull("amounts") + transfer.logIndex?.let { putDouble("logIndex", it.toDouble()) } ?: putNull("logIndex") + transfer.amountsUSD?.let { putArray("amountsUSD", stringArray(it)) } ?: putNull("amountsUSD") + transfer.pricesUSD?.let { putArray("pricesUSD", stringArray(it)) } ?: putNull("pricesUSD") + transfer.contractInfo?.let { putMap("contractInfo", tokenContractInfoMap(it)) } ?: putNull("contractInfo") + transfer.tokenMetadata?.let { putMap("tokenMetadata", tokenMetadataRecordMap(it)) } ?: putNull("tokenMetadata") + } + + private fun tokenMetadataRecordMap(value: Map): WritableMap = + Arguments.createMap().apply { + value.forEach { (key, metadata) -> putMap(key, tokenMetadataMap(metadata)) } + } + + private fun stringArray(value: Iterable): WritableArray = + Arguments.createArray().apply { + value.forEach { pushString(it) } + } + private fun transactionStatusMap(result: TransactionStatusResponse): WritableMap = Arguments.createMap().apply { putString("status", result.status.wireValue) @@ -828,7 +931,7 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : putString("symbol", token.symbol) putString("type", token.type) token.decimals?.let { putDouble("decimals", it.toDouble()) } ?: putNull("decimals") - putString("logoUrl", token.logoUrl) + putNullableString("logoUrl", token.logoUrl) putNullableString("contractAddress", token.contractAddress) putNullableString("tokenId", token.tokenId) } @@ -925,6 +1028,70 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : return jsonObject.mapValues { (_, item) -> item.jsonPrimitive.content } } + private fun String.toJsonObject(name: String): JsonObject { + val element = json.parseToJsonElement(this) + return element as? JsonObject ?: error("$name must be a JSON object") + } + + private fun JsonObject.requiredStringParam(name: String): String = + stringParam(name) ?: error("$name is required") + + private fun JsonObject.stringParam(name: String): String? = + this[name]?.takeUnless { it is JsonNull }?.jsonPrimitive?.contentOrNull + + private fun JsonObject.booleanParam(name: String): Boolean? = + this[name]?.takeUnless { it is JsonNull }?.jsonPrimitive?.contentOrNull?.toBooleanStrictOrNull() + + private fun JsonObject.intParam(name: String): Int? = + this[name]?.takeUnless { it is JsonNull }?.jsonPrimitive?.contentOrNull?.toIntOrNull()?.takeIf { it >= 0 } + ?: this[name]?.takeUnless { it is JsonNull }?.let { error("$name must be a non-negative integer") } + + private fun JsonObject.longParam(name: String): Long? = + this[name]?.takeUnless { it is JsonNull }?.jsonPrimitive?.contentOrNull?.toLongOrNull()?.takeIf { it >= 0 } + ?: this[name]?.takeUnless { it is JsonNull }?.let { error("$name must be a non-negative integer") } + + private fun JsonObject.stringListParam(name: String): List = + this[name] + ?.takeUnless { it is JsonNull } + ?.jsonArray + ?.map { it.jsonPrimitive.content } + ?: emptyList() + + private fun JsonObject.objectParam(name: String): JsonObject? = + this[name]?.takeUnless { it is JsonNull } as? JsonObject + + private fun JsonObject.networksParam(client: OMSClient): List = + stringListParam("networks").map { client.requireNetwork(it) } + + private fun JsonObject.indexerNetworkTypeParam(name: String): IndexerNetworkType? = + stringParam(name)?.let { value -> + IndexerNetworkType.entries.firstOrNull { it.wireValue == value } + ?: error("Unsupported indexer network type: $value") + } + + private fun JsonObject.contractVerificationStatusParam(name: String): ContractVerificationStatus? = + stringParam(name)?.let { value -> + ContractVerificationStatus.entries.firstOrNull { it.wireValue == value } + ?: error("Unsupported contract verification status: $value") + } + + private fun JsonObject.metadataOptionsParam(name: String): MetadataOptions? { + val value = objectParam(name) ?: return null + return MetadataOptions( + verifiedOnly = value.booleanParam("verifiedOnly"), + unverifiedOnly = value.booleanParam("unverifiedOnly"), + includeContracts = value.stringListParam("includeContracts") + ) + } + + private fun JsonObject.tokenBalancesPageRequestParam(): TokenBalancesPageRequest { + val value = objectParam("page") ?: return TokenBalancesPageRequest() + return TokenBalancesPageRequest( + page = value.intParam("page") ?: 0, + pageSize = value.intParam("pageSize") ?: 40 + ) + } + private fun String.toOidcProviderConfig(): OidcProviderConfig { val value = json.parseToJsonElement(this).jsonObject return OidcProviderConfig( @@ -1037,7 +1204,17 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : putNullableString("operation", operation?.id) status?.let { putDouble("status", it.toDouble()) } ?: putNull("status") putNullableString("txnId", txnId) - putBoolean("retryable", retryable) + retryable?.let { putBoolean("retryable", it) } ?: putNull("retryable") + upstreamError?.let { putMap("upstreamError", upstreamErrorMap(it)) } ?: putNull("upstreamError") + } + + private fun upstreamErrorMap(error: OmsUpstreamError): WritableMap = + Arguments.createMap().apply { + putString("service", error.service.name) + putNullableString("name", error.name) + putNullableString("code", error.code) + putNullableString("message", error.message) + error.status?.let { putDouble("status", it.toDouble()) } ?: putNull("status") } private fun OmsSdkErrorCode.bridgeCode(): String { diff --git a/examples/sdk-example/android/build.gradle b/examples/sdk-example/android/build.gradle index aa4c7b2..57d4e85 100644 --- a/examples/sdk-example/android/build.gradle +++ b/examples/sdk-example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { compileSdkVersion = 36 targetSdkVersion = 36 ndkVersion = "27.1.12297006" - kotlinVersion = "2.2.10" + kotlinVersion = "2.3.20" } repositories { mavenLocal() @@ -15,7 +15,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } diff --git a/examples/sdk-example/ios/Podfile.lock b/examples/sdk-example/ios/Podfile.lock index dea25e0..d68c08f 100644 --- a/examples/sdk-example/ios/Podfile.lock +++ b/examples/sdk-example/ios/Podfile.lock @@ -3,10 +3,10 @@ PODS: - hermes-engine (250829098.0.10): - hermes-engine/Pre-built (= 250829098.0.10) - hermes-engine/Pre-built (250829098.0.10) - - oms-client-swift-sdk (0.1.0-alpha.2) - - OmsClientReactNativeSdk (0.1.0-alpha.1): + - oms-client-swift-sdk (0.1.0-alpha.3) + - OmsClientReactNativeSdk (0.1.0-alpha.3): - hermes-engine - - oms-client-swift-sdk (= 0.1.0-alpha.2) + - oms-client-swift-sdk (= 0.1.0-alpha.3) - RCTRequired - RCTTypeSafety - React-Core @@ -2056,81 +2056,81 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d hermes-engine: 050bf0e0b60ac8f6b274d047bab7d63320f019cc - oms-client-swift-sdk: 6e4663b82d54091c97e7670caf5d3c2d40fdbad3 - OmsClientReactNativeSdk: f54b5bd0f5290070984706a6a70cb8052b5f9395 + oms-client-swift-sdk: af780f1aef17bf49d824cf21abe49908a54f87bb + OmsClientReactNativeSdk: 76f26489d4d7da9dbd96db6916f740afa4d4af57 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702 - RCTSwiftUIWrapper: 966ca7f5f22ac0b2b2255fb09cffc381f5440b03 + RCTSwiftUIWrapper: 7b8a1ffba8994a8f955c563511bffb74b4681317 RCTTypeSafety: 2a6403ba3492c04510e7c15bd635461646c43bb2 React: e2dc35338068bbd299c66f043ae0d7f25de8499e React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48 - React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0 + React-Core: f90d375d3bab515ad00df30605ce1bf02e6db12f React-Core-prebuilt: bd567b119737176084436942e59dc87cd8b0c7f7 - React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146 - React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716 + React-CoreModules: da4f80202ef954bdfb926ca4c9ac5f685900aa5c + React-cxxreact: 1d1d2a28a5f10e4e2e7cf2bfd54dc9c92c68c672 React-debug: 92944dc4d89f56d640e75498266cbde557a48189 - React-defaultsnativemodule: cd64bc09d7ca24112bbaf1b91edbbcf3d81ea7dc - React-domnativemodule: dacf5bc055ae041039574f38b73a20b91e368774 - React-Fabric: bb0baa33d91839631d315800eb23e9aaa4338a44 - React-FabricComponents: c504d0b0e2f3054b2ba19af839f175cb361153c0 - React-FabricImage: 91eaea1cc58d25ae2596a9277bcfe028f92374c3 - React-featureflags: 5ac0455da0af12ca79b40402e2f42c5c7556b638 - React-featureflagsnativemodule: df7da181b064f10f5959a7cd529b5aab3686ecb2 - React-graphics: d25b1195baf24c7918543f4aff9be89cc080906f - React-hermes: 663286153a8ad6bf752b742654c766ff5e8991e7 - React-idlecallbacksnativemodule: 0bd5392cb67f1ab25df736814b7c05213b1d3c68 - React-ImageManager: a03eed3e3d4222130dc0ad503a1a5f3aa89de746 - React-intersectionobservernativemodule: 5d0f1c3c7b30031b0f6f730ee52dd9d607e2c966 - React-jserrorhandler: d5d6e7e20c5a2d6e8607e18d31d8712d7de676f6 - React-jsi: eb7cc4cffcf24796cc302d5b2bca0e92544139a9 - React-jsiexecutor: 65006f60e64c72c6b82f62ef6bd17c84846e73f8 - React-jsinspector: 01e32e2247b2486117fbb143db7f7717ef462c6d - React-jsinspectorcdp: f1cbb34ca41d188ba22efd9b663cf258a911f6cd - React-jsinspectornetwork: f61acc94c881c41451f508abfe6efa748b956c21 - React-jsinspectortracing: 9395894d9bd4d17931b9afcf230c0e7f4cb3d674 - React-jsitooling: 03ead841daa12a93b18479f5e400ceab3732d36d - React-jsitracing: 4ae61c79e14360d1c6ab566031c62c490da78439 - React-logger: bf149dea4343a9037b74bade36cced8b63f03f46 - React-Mapbuffer: fec3e025f0ffba6b32cd2a1d7bbdee3e269aae90 - React-microtasksnativemodule: ab33a818d339f5a1da308893c11b487be66121a8 - React-mutationobservernativemodule: a42d1626651ccd7d0dc02a56e69d4ec77c248893 - React-NativeModulesApple: deba264b03bd79c6bd61014fa30e40321b5e443a - React-networking: 35e6070b084f435429f85c5db40b4d5b38652fe9 + React-defaultsnativemodule: 172d2ef7d11c531c40ee74f3be1ac98d258a817f + React-domnativemodule: 5a60a88075a35e20fa5879c94e7aa24e5f4071d0 + React-Fabric: 5c1954e06c094623e46d51da2dc2c926621c03a5 + React-FabricComponents: f32494150868073accd5d52d46b30a0496626813 + React-FabricImage: dfcd2826b10f05c19e5bfa68d4937c4401c129db + React-featureflags: 84bcfcb8f91f9475e437f3dd579399085a4af51e + React-featureflagsnativemodule: 83111c4ee188b8859aaa8643c1be640442098fef + React-graphics: e0441bc695f5c682a3fb1b0fd317e10765700f12 + React-hermes: da795ff5d5816d8940fca927c46dcdd81d7e9ad6 + React-idlecallbacksnativemodule: b6d79e1c9e335564e17d18e2649fe7524c4e74e4 + React-ImageManager: 0e137d7231092ddaa236f3520b67b4aa833e7079 + React-intersectionobservernativemodule: 160b3c85bb5a3df2bf50e60619c0db50aadbef8c + React-jserrorhandler: 619d382632f5ed166541c03f7ba7deee76fb1b16 + React-jsi: eac528e72d25146faa922ef1aad82d338addbb75 + React-jsiexecutor: 2d3000fdde95d0da451f8976cdf9018448756023 + React-jsinspector: 0377987f9635c64c5aaf72ec73aaf2b0b0da7744 + React-jsinspectorcdp: 9db641a9cd0dd5b693df27d2e665ac2540ddc336 + React-jsinspectornetwork: 61b00d0adfe6f175830660f1b98da576bc74eb0a + React-jsinspectortracing: 0f8d4f2ebd7523aece78cbc926cd4b6f2fc227dc + React-jsitooling: 36f3854063d507bc4d4a01227ebf1b63d9e24592 + React-jsitracing: cddf044c0c2847d3f5822a984018fd5596c1d448 + React-logger: 57001aefabd79d885589d2f31a8be05ccc393eaa + React-Mapbuffer: 1aa9126122d4247ffc24bf9d28d50ce923499a71 + React-microtasksnativemodule: d86581169e9bb5bb6f5fc3c5052f890016c1bf21 + React-mutationobservernativemodule: 9a0c4e866f1ef2a57acebc902ebacbdf469ae741 + React-NativeModulesApple: cc6ec4767844d610e92cc358bd3ea34937438d56 + React-networking: a8ce15641ed7775d5b54a9d0d32defc367c2216e React-oscompat: 64a0c7ef5441855dc6e2a6afe8ba8f92aa05075e - React-perflogger: ca04287f205086a1edb5c95882be7b6068458889 - React-performancecdpmetrics: cf1d0a3178ccd59353cedcacbda421f40100a889 - React-performancetimeline: c9771212e7a43032d6f8d5edfb58280d46a7ce1f + React-perflogger: b2b0144e4ba8dd157c1e90f8ebb02d22edbd040d + React-performancecdpmetrics: 5a715ec33f2dea48a93a70db37a78b4f51f9fd5a + React-performancetimeline: c292077a6fbc7f423269b01aab0fb7cc48efe3c0 React-RCTActionSheet: ab545c1e7b5f1ce4f8b40b6fa06afe2869095884 - React-RCTAnimation: 343147a9cd68c93d0ca280799fedfc7102d76ce4 - React-RCTAppDelegate: 5054754e92aaa9f8bfabe0f1022b84e46f3dcb57 - React-RCTBlob: 8c7ae3422ca4e72bc64b7a0142fd730efc5d4dfb - React-RCTFabric: be458db054b206c4d8e4f20f666e75d5f2c2d420 - React-RCTFBReactNativeSpec: 06db2e8d0f352d9fa23321aed1dd2cde25a3e83c - React-RCTImage: 481457bca63e039eb997f7d16c7560472f49657e - React-RCTLinking: 76cbb871240cec2dc5e7aa26c60f59e0ebbcf5a2 - React-RCTNetwork: e23a778225b7672e38545d3c5c24e1f4aad6d15f - React-RCTRuntime: 93c830b3ab3f7b494bbe7ae7289784f8b07b3947 - React-RCTSettings: 96196b535bef147381f96cc60ce9bda85d8be848 - React-RCTText: 749ebbd1a999fd84d80f37002ea3bf597fcea6df - React-RCTVibration: 5b41a7f274757c2928845981d970916ef9e4ca14 + React-RCTAnimation: 1f9a58c7b74c25ea4ca6e6fbd05f37cc4919097a + React-RCTAppDelegate: 856f82c57b47c1795009af585cfb7b87fe2d3b5a + React-RCTBlob: 1cb18861a19be0b4d7e44bed9315e791413399f9 + React-RCTFabric: 081853d9efb9b38fa868c1ff3d60e48106fe2a24 + React-RCTFBReactNativeSpec: cbc760c7a889bfce20746ca0037b97c10ea58665 + React-RCTImage: 0d94b22644ca87fcb778a8fa3ffbf990bc9028df + React-RCTLinking: 881a0c6772dd05a14ab0edfea05dadb698ec8090 + React-RCTNetwork: feb412861e3fff3426eb0961c2acdd96bee8dce1 + React-RCTRuntime: c61b848ffadc0209fe643406370d58021bd3585d + React-RCTSettings: b6e14b8764f807ebe9be2e7f0d72be83b359ac26 + React-RCTText: 1ecd4f85ff609183ebec858cf94c0f27a9dec163 + React-RCTVibration: 3611aa5cb59094632b3141d72c50cbb8ce3d5b08 React-rendererconsistency: 6708acd4bc39c1c5b00164370d0010d93b324c1f - React-renderercss: 80eb778756fed511d6128fc005188ac9008b0baf - React-rendererdebug: b46f338fb9d3f0bea6cf0621016c6c5a7a18e72e - React-RuntimeApple: c494b2089fad4a0c553cf63c2bb265f7eca2285d - React-RuntimeCore: b0bb151c3e2b26c6309d45d05c54aa65a6e0c094 - React-runtimeexecutor: 00b18635b6216a1708f6eb35dbadfd993ec91c7b - React-RuntimeHermes: bb44c4c574ce1b9507cad2e6be015344d18b94a9 - React-runtimescheduler: e1631e57209cb94b3efc29002b6a049cac3f6599 - React-timing: 356b88317ca60d373b0d94b6e7a71b0a572899f5 - React-utils: ccc01da318979af773259c4f6cdb1876f6f86f1a - React-webperformancenativemodule: f8b97c2cb6cfa94e92a503c09ad6d491c50a1390 - ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2 - ReactCodegen: c8f81e6c6f762dcf442a6203a1fb58f7dafc8014 - ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f + React-renderercss: d4a70898381431dcc07d6deba94a7eaef0cc9058 + React-rendererdebug: ad92235a960983f69183e2e59e2bce21e72c80a0 + React-RuntimeApple: 53a5b8fe60b044ec6fd4d254d66b7da69314a295 + React-RuntimeCore: 97e125471c0b8313db2bbeb1e9dc001a5503e979 + React-runtimeexecutor: 0ff42dcf1cc4bc7a89d29532a16a7d2d3849fb2f + React-RuntimeHermes: da5a1b7e39f3db1a7b3303d75d4c5bb6cd7c0d49 + React-runtimescheduler: 0e24c80f0c8b6e822a33e4ad89e0ab555704eedb + React-timing: f23e82ad46673716e34a786048414ab80f237594 + React-utils: 2851415c698da4b18331f9a445825d260fffa32c + React-webperformancenativemodule: 9f29ed34f6f6c01dac688b95fd2dfcbccf3aed7a + ReactAppDependencyProvider: a54c0c9b976766e1b6d5e2bb4d1ad0d15913e9e1 + ReactCodegen: 9af5798e943781fd2318dab7b60f25337388047c + ReactCommon: 3ec2a55999d296af90c24beb66c8a77dc660d3ef ReactNativeDependencies: aa020a3b32eb01c8ad2837b6b1d592b9d4f3ba85 - RNInAppBrowser: 904d24dc75e8e6c6c98a3160329192608946f9df - Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b + RNInAppBrowser: b53e6f6072c931115bc22ac9dc9510ac2cbea62d + Yoga: 36fee8f1fca3f54a28c3d7f80e69f66d73d3af96 PODFILE CHECKSUM: 287c20e8b3351f7d564be9fd7544afe469845e93 diff --git a/examples/sdk-example/src/App.tsx b/examples/sdk-example/src/App.tsx index 7c95f2d..986a932 100644 --- a/examples/sdk-example/src/App.tsx +++ b/examples/sdk-example/src/App.tsx @@ -19,19 +19,8 @@ import { } from 'react-native'; import { InAppBrowser } from 'react-native-inappbrowser-reborn'; import { - completeEmailAuth, - configure, - getSupportedNetworks, - getSession, - sendTransaction, - handleOidcRedirectCallback, + OMSClient, OidcProviders, - onSessionExpired, - signMessage, - signOut, - startEmailAuth, - startOidcRedirectAuth, - verifyMessageSignature, type OmsClientSessionExpiredEvent, type OmsClientSessionState, type OmsFeeOptionSelection, @@ -42,13 +31,9 @@ import { type OmsWalletActivationResult, } from '@0xsequence/oms-react-native-sdk'; -const DEMO_PUBLISHABLE_KEY = 'AQAAAAAAAAK2JvvZhWqZ51riasWBftkrVXE'; -const DEMO_PROJECT_ID = 'proj_014kg56dc0a75'; +const DEMO_PUBLISHABLE_KEY = + 'pk_dev_sdbx_01kqa06hyyetj_01kv5ceg4xefattzmm9fyx04ev'; const DEMO_OIDC_REDIRECT_URI = 'omsclientrndemo://auth/callback'; -const DEMO_ENVIRONMENT = { - apiRpcUrl: 'https://dev-api.sequence.app/rpc/API', - indexerUrlTemplate: 'https://dev-{value}-indexer.sequence.app/rpc/Indexer/', -}; const DEFAULT_TRANSACTION_TO = '0xE5E8B483FfC05967FcFed58cc98D053265af6D99'; const PREFERRED_NETWORK_ORDER = ['80002', '137']; @@ -378,6 +363,10 @@ function NetworkPickerModal({ } export default function App() { + const oms = useMemo( + () => new OMSClient({ publishableKey: DEMO_PUBLISHABLE_KEY }), + [] + ); const [networks, setNetworks] = useState([]); const [selectedChainId, setSelectedChainId] = useState('80002'); const [sdkReady, setSdkReady] = useState(false); @@ -485,7 +474,7 @@ export default function App() { }, []); const refreshSession = useCallback(async () => { - const nextSession = await getSession(); + const nextSession = await oms.wallet.getSession(); setSession(nextSession); if (nextSession.walletAddress) { setExpiredSessionEvent(null); @@ -494,7 +483,7 @@ export default function App() { setTransactionStatus('Transaction status: ready to send.'); } return nextSession; - }, []); + }, [oms]); const runAction = useCallback( async ( @@ -556,7 +545,7 @@ export default function App() { const activateWallet = useCallback( async (result: OmsWalletActivationResult) => { - const nextSession = await getSession(); + const nextSession = await oms.wallet.getSession(); const address = nextSession.walletAddress ?? result.walletAddress; clearExpiredSessionState(); setPendingWalletSelection(null); @@ -572,7 +561,7 @@ export default function App() { setTransactionStatus('Transaction status: ready to send.'); appendLog(`Wallet ready: ${address}`); }, - [appendLog, clearExpiredSessionState] + [appendLog, clearExpiredSessionState, oms] ); const finishOidcRedirectSignIn = useCallback( @@ -588,7 +577,7 @@ export default function App() { let callbackHandled = false; try { setAuthStatus('Completing Google redirect sign-in...'); - const result = await handleOidcRedirectCallback({ + const result = await oms.wallet.handleOidcRedirectCallback({ callbackUrl, walletSelection: manualWalletSelection ? 'manual' : 'automatic', sessionLifetimeSeconds: requestedSessionLifetimeSeconds(), @@ -636,6 +625,7 @@ export default function App() { activateWallet, appendLog, manualWalletSelection, + oms, refreshSession, requestedSessionLifetimeSeconds, ] @@ -662,13 +652,7 @@ export default function App() { async function bootstrap() { await runAction('Initializing SDK', async () => { - await configure({ - publishableKey: DEMO_PUBLISHABLE_KEY, - projectId: DEMO_PROJECT_ID, - environment: DEMO_ENVIRONMENT, - }); - - const supportedNetworks = sortNetworks(await getSupportedNetworks()); + const supportedNetworks = sortNetworks(oms.supportedNetworks); if (disposed) return; setNetworks(supportedNetworks); @@ -688,12 +672,13 @@ export default function App() { return () => { disposed = true; }; - }, [appendLog, refreshSession, runAction]); + }, [appendLog, oms, refreshSession, runAction]); useEffect(() => { if (!sdkReady) return undefined; - const sessionExpiredSubscription = onSessionExpired(handleSessionExpired); + const sessionExpiredSubscription = + oms.wallet.onSessionExpired(handleSessionExpired); const subscription = Linking.addEventListener('url', ({ url }) => { if (isDemoOidcRedirectUrl(url)) { runAction( @@ -734,6 +719,7 @@ export default function App() { appendLog, finishOidcRedirectSignIn, handleSessionExpired, + oms, runAction, sdkReady, ]); @@ -754,7 +740,7 @@ export default function App() { if (!emailForSignIn) { throw new Error('Email is required'); } - await startEmailAuth(emailForSignIn); + await oms.wallet.startEmailAuth(emailForSignIn); setEmail(''); setAuthStage('code'); setAuthStatus(`Code requested for ${emailForSignIn}`); @@ -770,7 +756,7 @@ export default function App() { 'Confirm code and resolve wallet', async () => { setAuthStatus('Confirming code and resolving wallet...'); - const authResult = await completeEmailAuth({ + const authResult = await oms.wallet.completeEmailAuth({ code: requireText(code, 'Verification code'), walletSelection: manualWalletSelection ? 'manual' : 'automatic', sessionLifetimeSeconds: requestedSessionLifetimeSeconds(), @@ -803,7 +789,7 @@ export default function App() { setPendingWalletSelection(null); setAuthStatus('Opening Google redirect sign-in...'); requestedSessionLifetimeSeconds(); - const started = await startOidcRedirectAuth({ + const started = await oms.wallet.startOidcRedirectAuth({ provider: OidcProviders.google(), redirectUri: DEMO_OIDC_REDIRECT_URI, loginHint: expiredSessionEmail(expiredSessionEvent), @@ -849,7 +835,7 @@ export default function App() { const cancelCodeStep = () => { runAction('Cancel email code step', async () => { - await signOut(); + await oms.wallet.signOut(); clearExpiredSessionState(); setSession(SIGNED_OUT_SESSION); setCode(''); @@ -891,7 +877,7 @@ export default function App() { const logout = () => { runAction('Logout', async () => { - await signOut(); + await oms.wallet.signOut(); clearExpiredSessionState(); setSession(SIGNED_OUT_SESSION); setAuthStage('email'); @@ -913,7 +899,10 @@ export default function App() { const network = requireNetwork(selectedNetwork); const nextMessage = requireText(message, 'Message'); setSignatureStatus('Signature status: signing in progress...'); - const signature = await signMessage(network.chainId, nextMessage); + const signature = await oms.wallet.signMessage( + network.chainId, + nextMessage + ); setLastSignedMessage(nextMessage); setLastSignature(signature); setSignatureStatus('Signature status: signed. Ready to verify.'); @@ -933,7 +922,7 @@ export default function App() { const signedMessage = requireText(lastSignedMessage, 'Signed message'); const signature = requireText(lastSignature, 'Signature'); setSignatureStatus('Signature status: verification in progress...'); - const isValid = await verifyMessageSignature({ + const isValid = await oms.wallet.verifyMessageSignature({ chainId: network.chainId, message: signedMessage, signature, @@ -957,7 +946,7 @@ export default function App() { async () => { const network = requireNetwork(selectedNetwork); setTransactionStatus('Transaction status: sending in progress...'); - const txResult = await sendTransaction({ + const txResult = await oms.wallet.sendTransaction({ chainId: network.chainId, to: requireText(transactionTo, 'Transaction destination'), value: decimalToBaseUnits(transactionValue, 18), diff --git a/examples/trails-actions-example/android/build.gradle b/examples/trails-actions-example/android/build.gradle index aa4c7b2..57d4e85 100644 --- a/examples/trails-actions-example/android/build.gradle +++ b/examples/trails-actions-example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { compileSdkVersion = 36 targetSdkVersion = 36 ndkVersion = "27.1.12297006" - kotlinVersion = "2.2.10" + kotlinVersion = "2.3.20" } repositories { mavenLocal() @@ -15,7 +15,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } diff --git a/examples/trails-actions-example/ios/Podfile.lock b/examples/trails-actions-example/ios/Podfile.lock index 16ff805..1929de7 100644 --- a/examples/trails-actions-example/ios/Podfile.lock +++ b/examples/trails-actions-example/ios/Podfile.lock @@ -3,10 +3,10 @@ PODS: - hermes-engine (250829098.0.10): - hermes-engine/Pre-built (= 250829098.0.10) - hermes-engine/Pre-built (250829098.0.10) - - oms-client-swift-sdk (0.1.0-alpha.2) - - OmsClientReactNativeSdk (0.1.0-alpha.1): + - oms-client-swift-sdk (0.1.0-alpha.3) + - OmsClientReactNativeSdk (0.1.0-alpha.3): - hermes-engine - - oms-client-swift-sdk (= 0.1.0-alpha.2) + - oms-client-swift-sdk (= 0.1.0-alpha.3) - RCTRequired - RCTTypeSafety - React-Core @@ -2081,82 +2081,82 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d hermes-engine: 90b235deeeedbd59b4f8ea505512fe1c61b464f7 - oms-client-swift-sdk: 6e4663b82d54091c97e7670caf5d3c2d40fdbad3 - OmsClientReactNativeSdk: f54b5bd0f5290070984706a6a70cb8052b5f9395 + oms-client-swift-sdk: af780f1aef17bf49d824cf21abe49908a54f87bb + OmsClientReactNativeSdk: 76f26489d4d7da9dbd96db6916f740afa4d4af57 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702 - RCTSwiftUIWrapper: 966ca7f5f22ac0b2b2255fb09cffc381f5440b03 + RCTSwiftUIWrapper: 7b8a1ffba8994a8f955c563511bffb74b4681317 RCTTypeSafety: 2a6403ba3492c04510e7c15bd635461646c43bb2 React: e2dc35338068bbd299c66f043ae0d7f25de8499e React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48 - React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0 + React-Core: f90d375d3bab515ad00df30605ce1bf02e6db12f React-Core-prebuilt: fdd2cc5551598e6cd3fc13aa50a66b922aaa1e20 - React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146 - React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716 + React-CoreModules: da4f80202ef954bdfb926ca4c9ac5f685900aa5c + React-cxxreact: 1d1d2a28a5f10e4e2e7cf2bfd54dc9c92c68c672 React-debug: 92944dc4d89f56d640e75498266cbde557a48189 - React-defaultsnativemodule: cd64bc09d7ca24112bbaf1b91edbbcf3d81ea7dc - React-domnativemodule: dacf5bc055ae041039574f38b73a20b91e368774 - React-Fabric: bb0baa33d91839631d315800eb23e9aaa4338a44 - React-FabricComponents: c504d0b0e2f3054b2ba19af839f175cb361153c0 - React-FabricImage: 91eaea1cc58d25ae2596a9277bcfe028f92374c3 - React-featureflags: 5ac0455da0af12ca79b40402e2f42c5c7556b638 - React-featureflagsnativemodule: df7da181b064f10f5959a7cd529b5aab3686ecb2 - React-graphics: d25b1195baf24c7918543f4aff9be89cc080906f - React-hermes: 663286153a8ad6bf752b742654c766ff5e8991e7 - React-idlecallbacksnativemodule: 0bd5392cb67f1ab25df736814b7c05213b1d3c68 - React-ImageManager: a03eed3e3d4222130dc0ad503a1a5f3aa89de746 - React-intersectionobservernativemodule: 5d0f1c3c7b30031b0f6f730ee52dd9d607e2c966 - React-jserrorhandler: d5d6e7e20c5a2d6e8607e18d31d8712d7de676f6 - React-jsi: eb7cc4cffcf24796cc302d5b2bca0e92544139a9 - React-jsiexecutor: 65006f60e64c72c6b82f62ef6bd17c84846e73f8 - React-jsinspector: 01e32e2247b2486117fbb143db7f7717ef462c6d - React-jsinspectorcdp: f1cbb34ca41d188ba22efd9b663cf258a911f6cd - React-jsinspectornetwork: f61acc94c881c41451f508abfe6efa748b956c21 - React-jsinspectortracing: 9395894d9bd4d17931b9afcf230c0e7f4cb3d674 - React-jsitooling: 03ead841daa12a93b18479f5e400ceab3732d36d - React-jsitracing: 4ae61c79e14360d1c6ab566031c62c490da78439 - React-logger: bf149dea4343a9037b74bade36cced8b63f03f46 - React-Mapbuffer: fec3e025f0ffba6b32cd2a1d7bbdee3e269aae90 - React-microtasksnativemodule: ab33a818d339f5a1da308893c11b487be66121a8 - React-mutationobservernativemodule: a42d1626651ccd7d0dc02a56e69d4ec77c248893 - react-native-webview: 2da09bdea1aeb6c46e27dd94632e21059cade6c1 - React-NativeModulesApple: deba264b03bd79c6bd61014fa30e40321b5e443a - React-networking: 35e6070b084f435429f85c5db40b4d5b38652fe9 + React-defaultsnativemodule: 172d2ef7d11c531c40ee74f3be1ac98d258a817f + React-domnativemodule: 5a60a88075a35e20fa5879c94e7aa24e5f4071d0 + React-Fabric: 5c1954e06c094623e46d51da2dc2c926621c03a5 + React-FabricComponents: f32494150868073accd5d52d46b30a0496626813 + React-FabricImage: dfcd2826b10f05c19e5bfa68d4937c4401c129db + React-featureflags: 84bcfcb8f91f9475e437f3dd579399085a4af51e + React-featureflagsnativemodule: 83111c4ee188b8859aaa8643c1be640442098fef + React-graphics: e0441bc695f5c682a3fb1b0fd317e10765700f12 + React-hermes: da795ff5d5816d8940fca927c46dcdd81d7e9ad6 + React-idlecallbacksnativemodule: b6d79e1c9e335564e17d18e2649fe7524c4e74e4 + React-ImageManager: 0e137d7231092ddaa236f3520b67b4aa833e7079 + React-intersectionobservernativemodule: 160b3c85bb5a3df2bf50e60619c0db50aadbef8c + React-jserrorhandler: 619d382632f5ed166541c03f7ba7deee76fb1b16 + React-jsi: eac528e72d25146faa922ef1aad82d338addbb75 + React-jsiexecutor: 2d3000fdde95d0da451f8976cdf9018448756023 + React-jsinspector: 0377987f9635c64c5aaf72ec73aaf2b0b0da7744 + React-jsinspectorcdp: 9db641a9cd0dd5b693df27d2e665ac2540ddc336 + React-jsinspectornetwork: 61b00d0adfe6f175830660f1b98da576bc74eb0a + React-jsinspectortracing: 0f8d4f2ebd7523aece78cbc926cd4b6f2fc227dc + React-jsitooling: 36f3854063d507bc4d4a01227ebf1b63d9e24592 + React-jsitracing: cddf044c0c2847d3f5822a984018fd5596c1d448 + React-logger: 57001aefabd79d885589d2f31a8be05ccc393eaa + React-Mapbuffer: 1aa9126122d4247ffc24bf9d28d50ce923499a71 + React-microtasksnativemodule: d86581169e9bb5bb6f5fc3c5052f890016c1bf21 + React-mutationobservernativemodule: 9a0c4e866f1ef2a57acebc902ebacbdf469ae741 + react-native-webview: 474aeef432561cbd160483cc48796a212f4a9e22 + React-NativeModulesApple: cc6ec4767844d610e92cc358bd3ea34937438d56 + React-networking: a8ce15641ed7775d5b54a9d0d32defc367c2216e React-oscompat: 64a0c7ef5441855dc6e2a6afe8ba8f92aa05075e - React-perflogger: ca04287f205086a1edb5c95882be7b6068458889 - React-performancecdpmetrics: cf1d0a3178ccd59353cedcacbda421f40100a889 - React-performancetimeline: c9771212e7a43032d6f8d5edfb58280d46a7ce1f + React-perflogger: b2b0144e4ba8dd157c1e90f8ebb02d22edbd040d + React-performancecdpmetrics: 5a715ec33f2dea48a93a70db37a78b4f51f9fd5a + React-performancetimeline: c292077a6fbc7f423269b01aab0fb7cc48efe3c0 React-RCTActionSheet: ab545c1e7b5f1ce4f8b40b6fa06afe2869095884 - React-RCTAnimation: 343147a9cd68c93d0ca280799fedfc7102d76ce4 - React-RCTAppDelegate: 5054754e92aaa9f8bfabe0f1022b84e46f3dcb57 - React-RCTBlob: 8c7ae3422ca4e72bc64b7a0142fd730efc5d4dfb - React-RCTFabric: be458db054b206c4d8e4f20f666e75d5f2c2d420 - React-RCTFBReactNativeSpec: 06db2e8d0f352d9fa23321aed1dd2cde25a3e83c - React-RCTImage: 481457bca63e039eb997f7d16c7560472f49657e - React-RCTLinking: 76cbb871240cec2dc5e7aa26c60f59e0ebbcf5a2 - React-RCTNetwork: e23a778225b7672e38545d3c5c24e1f4aad6d15f - React-RCTRuntime: 93c830b3ab3f7b494bbe7ae7289784f8b07b3947 - React-RCTSettings: 96196b535bef147381f96cc60ce9bda85d8be848 - React-RCTText: 749ebbd1a999fd84d80f37002ea3bf597fcea6df - React-RCTVibration: 5b41a7f274757c2928845981d970916ef9e4ca14 + React-RCTAnimation: 1f9a58c7b74c25ea4ca6e6fbd05f37cc4919097a + React-RCTAppDelegate: 856f82c57b47c1795009af585cfb7b87fe2d3b5a + React-RCTBlob: 1cb18861a19be0b4d7e44bed9315e791413399f9 + React-RCTFabric: 081853d9efb9b38fa868c1ff3d60e48106fe2a24 + React-RCTFBReactNativeSpec: cbc760c7a889bfce20746ca0037b97c10ea58665 + React-RCTImage: 0d94b22644ca87fcb778a8fa3ffbf990bc9028df + React-RCTLinking: 881a0c6772dd05a14ab0edfea05dadb698ec8090 + React-RCTNetwork: feb412861e3fff3426eb0961c2acdd96bee8dce1 + React-RCTRuntime: c61b848ffadc0209fe643406370d58021bd3585d + React-RCTSettings: b6e14b8764f807ebe9be2e7f0d72be83b359ac26 + React-RCTText: 1ecd4f85ff609183ebec858cf94c0f27a9dec163 + React-RCTVibration: 3611aa5cb59094632b3141d72c50cbb8ce3d5b08 React-rendererconsistency: 6708acd4bc39c1c5b00164370d0010d93b324c1f - React-renderercss: 80eb778756fed511d6128fc005188ac9008b0baf - React-rendererdebug: b46f338fb9d3f0bea6cf0621016c6c5a7a18e72e - React-RuntimeApple: c494b2089fad4a0c553cf63c2bb265f7eca2285d - React-RuntimeCore: b0bb151c3e2b26c6309d45d05c54aa65a6e0c094 - React-runtimeexecutor: 00b18635b6216a1708f6eb35dbadfd993ec91c7b - React-RuntimeHermes: bb44c4c574ce1b9507cad2e6be015344d18b94a9 - React-runtimescheduler: e1631e57209cb94b3efc29002b6a049cac3f6599 - React-timing: 356b88317ca60d373b0d94b6e7a71b0a572899f5 - React-utils: ccc01da318979af773259c4f6cdb1876f6f86f1a - React-webperformancenativemodule: f8b97c2cb6cfa94e92a503c09ad6d491c50a1390 - ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2 - ReactCodegen: c8f81e6c6f762dcf442a6203a1fb58f7dafc8014 - ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f + React-renderercss: d4a70898381431dcc07d6deba94a7eaef0cc9058 + React-rendererdebug: ad92235a960983f69183e2e59e2bce21e72c80a0 + React-RuntimeApple: 53a5b8fe60b044ec6fd4d254d66b7da69314a295 + React-RuntimeCore: 97e125471c0b8313db2bbeb1e9dc001a5503e979 + React-runtimeexecutor: 0ff42dcf1cc4bc7a89d29532a16a7d2d3849fb2f + React-RuntimeHermes: da5a1b7e39f3db1a7b3303d75d4c5bb6cd7c0d49 + React-runtimescheduler: 0e24c80f0c8b6e822a33e4ad89e0ab555704eedb + React-timing: f23e82ad46673716e34a786048414ab80f237594 + React-utils: 2851415c698da4b18331f9a445825d260fffa32c + React-webperformancenativemodule: 9f29ed34f6f6c01dac688b95fd2dfcbccf3aed7a + ReactAppDependencyProvider: a54c0c9b976766e1b6d5e2bb4d1ad0d15913e9e1 + ReactCodegen: 9af5798e943781fd2318dab7b60f25337388047c + ReactCommon: 3ec2a55999d296af90c24beb66c8a77dc660d3ef ReactNativeDependencies: 9621027d00b5054d12f214da6fc23438e4e00280 - RNInAppBrowser: 904d24dc75e8e6c6c98a3160329192608946f9df - Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b + RNInAppBrowser: b53e6f6072c931115bc22ac9dc9510ac2cbea62d + Yoga: 36fee8f1fca3f54a28c3d7f80e69f66d73d3af96 PODFILE CHECKSUM: ba0252cbbc5c3feadec41532dd2f28c982104d60 diff --git a/examples/trails-actions-example/src/App.tsx b/examples/trails-actions-example/src/App.tsx index 68e39ea..337fd4b 100644 --- a/examples/trails-actions-example/src/App.tsx +++ b/examples/trails-actions-example/src/App.tsx @@ -44,18 +44,8 @@ import { type EarnMarket, } from '0xtrails/actions'; import { - completeEmailAuth, - configure, - getSession, - getSupportedNetworks, - getTokenBalances, - handleOidcRedirectCallback, + OMSClient, OidcProviders, - onSessionExpired, - sendTransaction, - signOut, - startEmailAuth, - startOidcRedirectAuth, type OmsClientSessionExpiredEvent, type OmsClientSessionState, type OmsNetwork, @@ -129,18 +119,13 @@ type DemoButtonProps = { style?: ViewStyle; }; -const DEMO_PUBLISHABLE_KEY = 'AQAAAAAAAAK2JvvZhWqZ51riasWBftkrVXE'; -const DEMO_PROJECT_ID = 'proj_014kg56dc0a75'; +const DEMO_PUBLISHABLE_KEY = + 'pk_dev_sdbx_01kqa06hyyetj_01kv5ceg4xefattzmm9fyx04ev'; const DEMO_OIDC_REDIRECT_URI = 'omsclientrndemo://auth/callback'; -const DEMO_ENVIRONMENT = { - apiRpcUrl: 'https://dev-api.sequence.app/rpc/API', - indexerUrlTemplate: 'https://dev-{value}-indexer.sequence.app/rpc/Indexer/', -}; const TRAILS_API_URL = 'https://trails-api.sequence.app'; const POLYGON_CHAIN_ID = '137'; const POLYGON_CHAIN_ID_NUMBER = 137; -const POLYGON_INDEXER_NAME = 'polygon'; const POLYGON_USDC = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; const POLYGON_WPOL = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'; const PREFERRED_NETWORK_ORDER = ['137']; @@ -820,51 +805,32 @@ function delay(ms: number): Promise { }); } -async function getNativePolBalanceRaw( - walletAddress: `0x${string}` -): Promise { - const indexerUrl = DEMO_ENVIRONMENT.indexerUrlTemplate - .replace('{value}', POLYGON_INDEXER_NAME) - .replace(/\/+$/, ''); - const response = await fetch(`${indexerUrl}/GetNativeTokenBalance`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'X-Access-Key': DEMO_PUBLISHABLE_KEY, - }, - body: JSON.stringify({ - accountAddress: walletAddress, - }), - }); - - if (!response.ok) { - throw new Error(`Native balance request failed with ${response.status}.`); - } - - const payload = (await response.json()) as { - balance?: { - balance?: string; - balanceWei?: string; - }; - }; - - return payload.balance?.balance ?? payload.balance?.balanceWei ?? '0'; -} - async function getPolygonBalances( + oms: OMSClient, walletAddress: `0x${string}` ): Promise { - const [polRaw, usdcResult] = await Promise.all([ - getNativePolBalanceRaw(walletAddress), - getTokenBalances({ - chainId: POLYGON_CHAIN_ID, - contractAddress: POLYGON_USDC, - walletAddress, - includeMetadata: false, - }), - ]); - const usdcRaw = usdcResult.balances[0]?.balance ?? '0'; + const polygonNetwork = oms.supportedNetworks.find( + (network) => network.chainId === POLYGON_CHAIN_ID + ); + if (!polygonNetwork) { + throw new Error('Polygon network is not available in this project.'); + } + + const result = await oms.indexer.getBalances({ + walletAddress, + networks: [polygonNetwork], + contractAddresses: [POLYGON_USDC], + includeMetadata: false, + }); + const polRaw = + result.nativeBalances.find( + (balance) => String(balance.chainId) === POLYGON_CHAIN_ID + )?.balance ?? '0'; + const usdcRaw = + result.balances.find( + (balance) => + balance.contractAddress?.toLowerCase() === POLYGON_USDC.toLowerCase() + )?.balance ?? '0'; return { pol: formatTokenAmount(polRaw, 18, 'POL'), @@ -1223,6 +1189,10 @@ async function prepareSwapAndEarnUsdc({ } export default function App() { + const oms = useMemo( + () => new OMSClient({ publishableKey: DEMO_PUBLISHABLE_KEY }), + [] + ); const [networks, setNetworks] = useState([]); const [sdkReady, setSdkReady] = useState(false); const [session, setSession] = @@ -1296,7 +1266,7 @@ export default function App() { }, []); const refreshSession = useCallback(async () => { - const nextSession = await getSession(); + const nextSession = await oms.wallet.getSession(); setSession(nextSession); if (nextSession.walletAddress) { setExpiredSessionEvent(null); @@ -1306,7 +1276,7 @@ export default function App() { setEarnStatus('Swap and Deposit status: ready to prepare.'); } return nextSession; - }, []); + }, [oms]); const clearExpiredSessionState = useCallback(() => { setExpiredSessionEvent(null); @@ -1369,7 +1339,7 @@ export default function App() { handlingRedirectUrlRef.current = callbackUrl; try { setAuthStatus('Completing Google redirect sign-in...'); - const result = await handleOidcRedirectCallback({ + const result = await oms.wallet.handleOidcRedirectCallback({ callbackUrl, walletSelection: 'automatic', }); @@ -1413,7 +1383,7 @@ export default function App() { handlingRedirectUrlRef.current = null; } }, - [appendLog, refreshSession] + [appendLog, oms, refreshSession] ); const refreshBalances = useCallback( @@ -1427,7 +1397,7 @@ export default function App() { })); try { - const nextBalances = await getPolygonBalances(walletAddress); + const nextBalances = await getPolygonBalances(oms, walletAddress); setBalances(nextBalances); return nextBalances; } catch (error) { @@ -1440,19 +1410,19 @@ export default function App() { return null; } }, - [appendLog] + [appendLog, oms] ); const readBalanceSnapshot = useCallback( async (walletAddress: `0x${string}`): Promise => { try { - return await getPolygonBalances(walletAddress); + return await getPolygonBalances(oms, walletAddress); } catch (error) { appendLog(`!! Balance snapshot failed: ${describeError(error)}`); return balances; } }, - [appendLog, balances] + [appendLog, balances, oms] ); const refreshEarnPositions = useCallback( @@ -1523,7 +1493,7 @@ export default function App() { await delay(BALANCE_POLL_INTERVAL_MS); try { - const nextBalances = await getPolygonBalances(walletAddress); + const nextBalances = await getPolygonBalances(oms, walletAddress); const isExpectedChange = hasExpectedBalanceChange( operation, before, @@ -1562,7 +1532,7 @@ export default function App() { })); throw new Error(message); }, - [] + [oms] ); const pollEarnPositionsUntilChanged = useCallback( @@ -1654,13 +1624,7 @@ export default function App() { async function bootstrap() { await runAction('Initializing SDK', async () => { - await configure({ - publishableKey: DEMO_PUBLISHABLE_KEY, - projectId: DEMO_PROJECT_ID, - environment: DEMO_ENVIRONMENT, - }); - - const supportedNetworks = sortNetworks(await getSupportedNetworks()); + const supportedNetworks = sortNetworks(oms.supportedNetworks); if (disposed) return; setNetworks(supportedNetworks); @@ -1688,12 +1652,13 @@ export default function App() { return () => { disposed = true; }; - }, [appendLog, refreshSession, runAction]); + }, [appendLog, oms, refreshSession, runAction]); useEffect(() => { if (!sdkReady) return undefined; - const sessionExpiredSubscription = onSessionExpired(handleSessionExpired); + const sessionExpiredSubscription = + oms.wallet.onSessionExpired(handleSessionExpired); const handleRedirectUrl = (url: string) => { if (!isDemoOidcRedirectUrl(url)) return; @@ -1731,6 +1696,7 @@ export default function App() { appendLog, finishOidcRedirectSignIn, handleSessionExpired, + oms.wallet, runAction, sdkReady, ]); @@ -1790,7 +1756,7 @@ export default function App() { throw new Error('Email is required'); } setAuthStatus('Requesting email code...'); - await startEmailAuth(emailForSignIn); + await oms.wallet.startEmailAuth(emailForSignIn); setEmail(''); setAuthStage('code'); setAuthStatus(`Code requested for ${emailForSignIn}`); @@ -1808,7 +1774,7 @@ export default function App() { setCode(''); setAuthStage('email'); setAuthStatus('Opening Google redirect sign-in...'); - const started = await startOidcRedirectAuth({ + const started = await oms.wallet.startOidcRedirectAuth({ provider: OidcProviders.google(), redirectUri: DEMO_OIDC_REDIRECT_URI, loginHint: expiredSessionEmail(expiredSessionEvent), @@ -1857,7 +1823,7 @@ export default function App() { 'Complete email sign-in', async () => { setAuthStatus('Verifying code...'); - await completeEmailAuth({ code: requireText(code, 'Code') }); + await oms.wallet.completeEmailAuth({ code: requireText(code, 'Code') }); const nextSession = await refreshSession(); clearExpiredSessionState(); setCode(''); @@ -1875,7 +1841,7 @@ export default function App() { const cancelCodeStep = () => { runAction('Cancel email sign-in', async () => { - await signOut(); + await oms.wallet.signOut(); clearExpiredSessionState(); setAuthStage('email'); setCode(''); @@ -1887,7 +1853,7 @@ export default function App() { const logout = () => { runAction('Sign out', async () => { - await signOut(); + await oms.wallet.signOut(); clearExpiredSessionState(); setSession(SIGNED_OUT_SESSION); setAuthStage('email'); @@ -2002,7 +1968,7 @@ export default function App() { const before = await readBalanceSnapshot(address); setSwapStatus('Swap status: sending...'); - const txResult = await sendTransaction({ + const txResult = await oms.wallet.sendTransaction({ chainId: POLYGON_CHAIN_ID, to: prepared.to, value: prepared.value, @@ -2049,7 +2015,7 @@ export default function App() { ? 'transaction' : `transaction ${index + 1}/${prepared.transactions.length}`; setDepositStatus(`Deposit status: sending ${label}...`); - const txResult = await sendTransaction({ + const txResult = await oms.wallet.sendTransaction({ chainId: String(transaction.chainId), to: transaction.to, value: transaction.value.toString(), @@ -2109,7 +2075,7 @@ export default function App() { const beforePositions = await readEarnPositionsSnapshot(address); setEarnStatus('Swap and Deposit status: sending...'); - const txResult = await sendTransaction({ + const txResult = await oms.wallet.sendTransaction({ chainId: POLYGON_CHAIN_ID, to: prepared.to, value: prepared.value, @@ -2172,7 +2138,7 @@ export default function App() { ? 'withdraw transaction' : `withdraw transaction ${index + 1}`; setEarnPositionsStatus(`Sending ${label}...`); - const txResult = await sendTransaction({ + const txResult = await oms.wallet.sendTransaction({ chainId: String(transaction.chainId), to: transaction.to, value: transaction.value.toString(), diff --git a/ios/OmsClientReactNativeSdk.mm b/ios/OmsClientReactNativeSdk.mm index 0c74471..37a54e4 100644 --- a/ios/OmsClientReactNativeSdk.mm +++ b/ios/OmsClientReactNativeSdk.mm @@ -33,83 +33,79 @@ - (instancetype)init return self; } -- (void)configure:(NSString *)publishableKey - walletApiUrl:(nullable NSString *)walletApiUrl - apiRpcUrl:(nullable NSString *)apiRpcUrl -indexerUrlTemplate:(nullable NSString *)indexerUrlTemplate - projectId:(NSString *)projectId - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject +- (void)createClient:(NSString *)clientId + publishableKey:(NSString *)publishableKey + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { - [_impl configureWithPublishableKey:publishableKey - walletApiUrl:walletApiUrl - apiRpcUrl:apiRpcUrl - indexerUrlTemplate:indexerUrlTemplate - projectId:projectId - resolve:resolve - reject:reject]; + [_impl createClientWithClientId:clientId + publishableKey:publishableKey + resolve:resolve + reject:reject]; } -- (void)getWalletAddress:(RCTPromiseResolveBlock)resolve +- (void)getWalletAddress:(NSString *)clientId + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl getWalletAddressWithResolve:resolve reject:reject]; + [_impl getWalletAddressWithClientId:clientId resolve:resolve reject:reject]; } -- (void)getSession:(RCTPromiseResolveBlock)resolve +- (void)getSession:(NSString *)clientId + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl getSessionWithResolve:resolve reject:reject]; -} - -- (void)getSupportedNetworks:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject -{ - [_impl getSupportedNetworksWithResolve:resolve reject:reject]; + [_impl getSessionWithClientId:clientId resolve:resolve reject:reject]; } -- (void)startEmailAuth:(NSString *)email +- (void)startEmailAuth:(NSString *)clientId + email:(NSString *)email resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl startEmailAuthWithEmail:email resolve:resolve reject:reject]; + [_impl startEmailAuthWithClientId:clientId email:email resolve:resolve reject:reject]; } -- (void)completeEmailAuth:(NSString *)code +- (void)completeEmailAuth:(NSString *)clientId + code:(NSString *)code walletSelection:(nullable NSString *)walletSelection walletType:(nullable NSString *)walletType sessionLifetimeSeconds:(nullable NSString *)sessionLifetimeSeconds resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl completeEmailAuthWithCode:code - walletSelection:walletSelection - walletType:walletType - sessionLifetimeSeconds:sessionLifetimeSeconds - resolve:resolve - reject:reject]; + [_impl completeEmailAuthWithClientId:clientId + code:code + walletSelection:walletSelection + walletType:walletType + sessionLifetimeSeconds:sessionLifetimeSeconds + resolve:resolve + reject:reject]; } -- (void)signInWithOidcIdToken:(NSString *)idToken +- (void)signInWithOidcIdToken:(NSString *)clientId + idToken:(NSString *)idToken issuer:(NSString *)issuer audience:(NSString *)audience walletSelection:(nullable NSString *)walletSelection - walletType:(nullable NSString *)walletType - sessionLifetimeSeconds:(nullable NSString *)sessionLifetimeSeconds + walletType:(nullable NSString *)walletType + sessionLifetimeSeconds:(nullable NSString *)sessionLifetimeSeconds resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl signInWithOidcIdTokenWithIdToken:idToken - issuer:issuer - audience:audience - walletSelection:walletSelection + [_impl signInWithOidcIdTokenWithClientId:clientId + idToken:idToken + issuer:issuer + audience:audience + walletSelection:walletSelection walletType:walletType - sessionLifetimeSeconds:sessionLifetimeSeconds - resolve:resolve - reject:reject]; + sessionLifetimeSeconds:sessionLifetimeSeconds + resolve:resolve + reject:reject]; } -- (void)startOidcRedirectAuth:(NSString *)providerJson +- (void)startOidcRedirectAuth:(NSString *)clientId + providerJson:(NSString *)providerJson redirectUri:(NSString *)redirectUri walletType:(nullable NSString *)walletType relayRedirectUri:(nullable NSString *)relayRedirectUri @@ -118,101 +114,121 @@ - (void)startOidcRedirectAuth:(NSString *)providerJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl startOidcRedirectAuthWithProviderJson:providerJson - redirectUri:redirectUri - walletType:walletType - relayRedirectUri:relayRedirectUri - authorizeParamsJson:authorizeParamsJson - loginHint:loginHint - resolve:resolve - reject:reject]; + [_impl startOidcRedirectAuthWithClientId:clientId + providerJson:providerJson + redirectUri:redirectUri + walletType:walletType + relayRedirectUri:relayRedirectUri + authorizeParamsJson:authorizeParamsJson + loginHint:loginHint + resolve:resolve + reject:reject]; } -- (void)handleOidcRedirectCallback:(nullable NSString *)callbackUrl +- (void)handleOidcRedirectCallback:(NSString *)clientId + callbackUrl:(nullable NSString *)callbackUrl walletSelection:(nullable NSString *)walletSelection sessionLifetimeSeconds:(nullable NSString *)sessionLifetimeSeconds resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl handleOidcRedirectCallbackWithCallbackUrl:callbackUrl - walletSelection:walletSelection - sessionLifetimeSeconds:sessionLifetimeSeconds - resolve:resolve - reject:reject]; + [_impl handleOidcRedirectCallbackWithClientId:clientId + callbackUrl:callbackUrl + walletSelection:walletSelection + sessionLifetimeSeconds:sessionLifetimeSeconds + resolve:resolve + reject:reject]; } -- (void)listWallets:(RCTPromiseResolveBlock)resolve +- (void)listWallets:(NSString *)clientId + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl listWalletsWithResolve:resolve reject:reject]; + [_impl listWalletsWithClientId:clientId resolve:resolve reject:reject]; } -- (void)useWallet:(NSString *)walletId +- (void)useWallet:(NSString *)clientId + walletId:(NSString *)walletId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl useWalletWithWalletId:walletId resolve:resolve reject:reject]; + [_impl useWalletWithClientId:clientId walletId:walletId resolve:resolve reject:reject]; } -- (void)createWallet:(nullable NSString *)walletType +- (void)createWallet:(NSString *)clientId + walletType:(nullable NSString *)walletType reference:(nullable NSString *)reference resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl createWalletWithWalletType:walletType - reference:reference - resolve:resolve - reject:reject]; + [_impl createWalletWithClientId:clientId + walletType:walletType + reference:reference + resolve:resolve + reject:reject]; } -- (void)selectWalletForPendingSelection:(NSString *)pendingSelectionId +- (void)selectWalletForPendingSelection:(NSString *)clientId + pendingSelectionId:(NSString *)pendingSelectionId walletId:(NSString *)walletId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl selectWalletForPendingSelectionWithPendingSelectionId:pendingSelectionId - walletId:walletId - resolve:resolve - reject:reject]; + [_impl selectWalletForPendingSelectionWithClientId:clientId + pendingSelectionId:pendingSelectionId + walletId:walletId + resolve:resolve + reject:reject]; } -- (void)createAndSelectWalletForPendingSelection:(NSString *)pendingSelectionId - reference:(nullable NSString *)reference - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject +- (void)createAndSelectWalletForPendingSelection:(NSString *)clientId + pendingSelectionId:(NSString *)pendingSelectionId + reference:(nullable NSString *)reference + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { - [_impl createAndSelectWalletForPendingSelectionWithPendingSelectionId:pendingSelectionId - reference:reference - resolve:resolve - reject:reject]; + [_impl createAndSelectWalletForPendingSelectionWithClientId:clientId + pendingSelectionId:pendingSelectionId + reference:reference + resolve:resolve + reject:reject]; } -- (void)signOut:(RCTPromiseResolveBlock)resolve +- (void)signOut:(NSString *)clientId + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl signOutWithResolve:resolve reject:reject]; + [_impl signOutWithClientId:clientId resolve:resolve reject:reject]; } -- (void)signMessage:(NSString *)chainId +- (void)signMessage:(NSString *)clientId + chainId:(NSString *)chainId message:(NSString *)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl signMessageWithChainId:chainId message:message resolve:resolve reject:reject]; + [_impl signMessageWithClientId:clientId + chainId:chainId + message:message + resolve:resolve + reject:reject]; } -- (void)signTypedData:(NSString *)chainId +- (void)signTypedData:(NSString *)clientId + chainId:(NSString *)chainId typedDataJson:(NSString *)typedDataJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl signTypedDataWithChainId:chainId - typedDataJson:typedDataJson - resolve:resolve - reject:reject]; + [_impl signTypedDataWithClientId:clientId + chainId:chainId + typedDataJson:typedDataJson + resolve:resolve + reject:reject]; } -- (void)sendTransaction:(NSString *)chainId +- (void)sendTransaction:(NSString *)clientId + chainId:(NSString *)chainId to:(NSString *)to value:(NSString *)value data:(nullable NSString *)data @@ -226,22 +242,24 @@ - (void)sendTransaction:(NSString *)chainId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl sendTransactionWithChainId:chainId - to:to - value:value - data:data - mode:mode - feeOptionSelectorId:feeOptionSelectorId - waitForStatus:waitForStatus - statusPollingTimeoutMs:statusPollingTimeoutMs - statusPollingIntervalMs:statusPollingIntervalMs - statusPollingFastIntervalMs:statusPollingFastIntervalMs - statusPollingFastPollCount:statusPollingFastPollCount - resolve:resolve - reject:reject]; -} - -- (void)callContract:(NSString *)chainId + [_impl sendTransactionWithClientId:clientId + chainId:chainId + to:to + value:value + data:data + mode:mode + feeOptionSelectorId:feeOptionSelectorId + waitForStatus:waitForStatus + statusPollingTimeoutMs:statusPollingTimeoutMs + statusPollingIntervalMs:statusPollingIntervalMs + statusPollingFastIntervalMs:statusPollingFastIntervalMs + statusPollingFastPollCount:statusPollingFastPollCount + resolve:resolve + reject:reject]; +} + +- (void)callContract:(NSString *)clientId + chainId:(NSString *)chainId contractAddress:(NSString *)contractAddress method:(NSString *)method argsJson:(nullable NSString *)argsJson @@ -255,133 +273,135 @@ - (void)callContract:(NSString *)chainId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl callContractWithChainId:chainId - contractAddress:contractAddress - method:method - argsJson:argsJson - mode:mode - feeOptionSelectorId:feeOptionSelectorId - waitForStatus:waitForStatus - statusPollingTimeoutMs:statusPollingTimeoutMs - statusPollingIntervalMs:statusPollingIntervalMs - statusPollingFastIntervalMs:statusPollingFastIntervalMs - statusPollingFastPollCount:statusPollingFastPollCount - resolve:resolve - reject:reject]; + [_impl callContractWithClientId:clientId + chainId:chainId + contractAddress:contractAddress + method:method + argsJson:argsJson + mode:mode + feeOptionSelectorId:feeOptionSelectorId + waitForStatus:waitForStatus + statusPollingTimeoutMs:statusPollingTimeoutMs + statusPollingIntervalMs:statusPollingIntervalMs + statusPollingFastIntervalMs:statusPollingFastIntervalMs + statusPollingFastPollCount:statusPollingFastPollCount + resolve:resolve + reject:reject]; } - (void)respondToFeeOptionSelection:(NSString *)requestId - selectionToken:(nullable NSString *)selectionToken - errorMessage:(nullable NSString *)errorMessage - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject + selectionToken:(nullable NSString *)selectionToken + errorMessage:(nullable NSString *)errorMessage + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { [_impl respondToFeeOptionSelectionWithRequestId:requestId - selectionToken:selectionToken + selectionToken:selectionToken errorMessage:errorMessage resolve:resolve reject:reject]; } -- (void)getTransactionStatus:(NSString *)txnId +- (void)getTransactionStatus:(NSString *)clientId + txnId:(NSString *)txnId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl getTransactionStatusWithTxnId:txnId resolve:resolve reject:reject]; + [_impl getTransactionStatusWithClientId:clientId txnId:txnId resolve:resolve reject:reject]; } -- (void)getTokenBalances:(NSString *)chainId - contractAddress:(nullable NSString *)contractAddress - walletAddress:(NSString *)walletAddress - includeMetadata:(BOOL)includeMetadata - page:(nullable NSString *)page - pageSize:(nullable NSString *)pageSize - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject +- (void)getBalances:(NSString *)clientId + paramsJson:(NSString *)paramsJson + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { - [_impl getTokenBalancesWithChainId:chainId - contractAddress:contractAddress - walletAddress:walletAddress - includeMetadata:includeMetadata - page:page - pageSize:pageSize - resolve:resolve - reject:reject]; + [_impl getBalancesWithClientId:clientId paramsJson:paramsJson resolve:resolve reject:reject]; } -- (void)getNativeTokenBalance:(NSString *)chainId - walletAddress:(NSString *)walletAddress +- (void)getTransactionHistory:(NSString *)clientId + paramsJson:(NSString *)paramsJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl getNativeTokenBalanceWithChainId:chainId - walletAddress:walletAddress - resolve:resolve - reject:reject]; + [_impl getTransactionHistoryWithClientId:clientId + paramsJson:paramsJson + resolve:resolve + reject:reject]; } -- (void)verifyMessageSignature:(NSString *)chainId +- (void)verifyMessageSignature:(NSString *)clientId + chainId:(NSString *)chainId message:(NSString *)message signature:(NSString *)signature resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl verifyMessageSignatureWithChainId:chainId - message:message - signature:signature - resolve:resolve - reject:reject]; + [_impl verifyMessageSignatureWithClientId:clientId + chainId:chainId + message:message + signature:signature + resolve:resolve + reject:reject]; } -- (void)verifyTypedDataSignature:(NSString *)chainId +- (void)verifyTypedDataSignature:(NSString *)clientId + chainId:(NSString *)chainId typedDataJson:(NSString *)typedDataJson signature:(NSString *)signature resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl verifyTypedDataSignatureWithChainId:chainId + [_impl verifyTypedDataSignatureWithClientId:clientId + chainId:chainId typedDataJson:typedDataJson signature:signature resolve:resolve reject:reject]; } -- (void)getIdToken:(nullable NSString *)ttlSeconds +- (void)getIdToken:(NSString *)clientId + ttlSeconds:(nullable NSString *)ttlSeconds customClaimsJson:(nullable NSString *)customClaimsJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl getIdTokenWithTtlSeconds:ttlSeconds - customClaimsJson:customClaimsJson - resolve:resolve - reject:reject]; + [_impl getIdTokenWithClientId:clientId + ttlSeconds:ttlSeconds + customClaimsJson:customClaimsJson + resolve:resolve + reject:reject]; } -- (void)listAccess:(nullable NSString *)pageSize +- (void)listAccess:(NSString *)clientId + pageSize:(nullable NSString *)pageSize resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl listAccessWithPageSize:pageSize resolve:resolve reject:reject]; + [_impl listAccessWithClientId:clientId pageSize:pageSize resolve:resolve reject:reject]; } -- (void)listAccessPage:(nullable NSString *)pageSize +- (void)listAccessPage:(NSString *)clientId + pageSize:(nullable NSString *)pageSize cursor:(nullable NSString *)cursor resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl listAccessPageWithPageSize:pageSize + [_impl listAccessPageWithClientId:clientId + pageSize:pageSize cursor:cursor resolve:resolve reject:reject]; } -- (void)revokeAccess:(NSString *)targetCredentialId +- (void)revokeAccess:(NSString *)clientId + targetCredentialId:(NSString *)targetCredentialId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [_impl revokeAccessWithTargetCredentialId:targetCredentialId - resolve:resolve - reject:reject]; + [_impl revokeAccessWithClientId:clientId + targetCredentialId:targetCredentialId + resolve:resolve + reject:reject]; } - (std::shared_ptr)getTurboModule: diff --git a/ios/OmsClientReactNativeSdkImpl.swift b/ios/OmsClientReactNativeSdkImpl.swift index 9d17ea8..c17f076 100644 --- a/ios/OmsClientReactNativeSdkImpl.swift +++ b/ios/OmsClientReactNativeSdkImpl.swift @@ -4,12 +4,13 @@ import React @objc(OmsClientReactNativeSdkImpl) public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { - private var client: OMSClient? + private var clients: [String: OMSClient] = [:] + private let clientsLock = NSLock() private var feeOptionSelectionRequestEmitter: ((NSDictionary) -> Void)? private var sessionExpiredEventEmitter: ((NSDictionary) -> Void)? private var pendingFeeOptionSelections: [String: CheckedContinuation] = [:] private let pendingFeeOptionSelectionsLock = NSLock() - private var pendingWalletSelections: [String: PendingWalletSelection] = [:] + private var pendingWalletSelections: [String: StoredPendingWalletSelection] = [:] private let pendingWalletSelectionsLock = NSLock() private static let defaultSessionLifetimeSeconds: UInt32 = 604_800 @@ -23,79 +24,77 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { sessionExpiredEventEmitter = emitter } - @objc(configureWithPublishableKey:walletApiUrl:apiRpcUrl:indexerUrlTemplate:projectId:resolve:reject:) - public func configure( + @objc(createClientWithClientId:publishableKey:resolve:reject:) + public func createClient( + clientId: String, publishableKey: String, - walletApiUrl: String?, - apiRpcUrl: String?, - indexerUrlTemplate: String?, - projectId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - let environment = OMSClientEnvironment( - walletApiUrl: walletApiUrl ?? OMSClientEnvironment.defaultWalletApiUrl, - apiRpcUrl: apiRpcUrl ?? OMSClientEnvironment.defaultApiRpcUrl, - indexerUrlTemplate: indexerUrlTemplate ?? OMSClientEnvironment.defaultIndexerUrlTemplate - ) - - clearPendingWalletSelections() - client = OMSClient( - publishableKey: publishableKey, - projectId: projectId, - environment: environment - ) - client?.wallet.onSessionExpired = { [weak self] event in - guard let self else { - return + do { + clearPendingWalletSelections(clientId: clientId) + let client = try OMSClient(publishableKey: publishableKey) + client.wallet.onSessionExpired = { [weak self] event in + guard let self else { + return + } + self.sessionExpiredEventEmitter?( + self.sessionExpiredEventDictionary(clientId: clientId, event: event) as NSDictionary + ) } - self.sessionExpiredEventEmitter?(self.sessionExpiredEventDictionary(event) as NSDictionary) + storeClient(client, clientId: clientId) + resolve(nil) + } catch { + rejectError(reject, error) } - resolve(nil) } - @objc(getWalletAddressWithResolve:reject:) + @objc(getWalletAddressWithClientId:resolve:reject:) public func getWalletAddress( + clientId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - guard let address = client?.wallet.walletAddress, !address.isEmpty else { - resolve(NSNull()) - return + do { + guard let address = try requireClient(clientId).wallet.walletAddress?.nonEmpty else { + resolve(NSNull()) + return + } + resolve(address) + } catch { + rejectError(reject, error) } - resolve(address) } - @objc(getSessionWithResolve:reject:) + @objc(getSessionWithClientId:resolve:reject:) public func getSession( + clientId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - resolve(sessionDictionary(client?.wallet.session)) - } - - @objc(getSupportedNetworksWithResolve:reject:) - public func getSupportedNetworks( - resolve: @escaping RCTPromiseResolveBlock, - reject: @escaping RCTPromiseRejectBlock - ) { - resolve(Network.supportedNetworks.map(networkDictionary)) + do { + resolve(sessionDictionary(try requireClient(clientId).wallet.session)) + } catch { + rejectError(reject, error) + } } - @objc(startEmailAuthWithEmail:resolve:reject:) + @objc(startEmailAuthWithClientId:email:resolve:reject:) public func startEmailAuth( + clientId: String, email: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in try await client.wallet.startEmailAuth(email: email) return nil } } - @objc(completeEmailAuthWithCode:walletSelection:walletType:sessionLifetimeSeconds:resolve:reject:) + @objc(completeEmailAuthWithClientId:code:walletSelection:walletType:sessionLifetimeSeconds:resolve:reject:) public func completeEmailAuth( + clientId: String, code: String, walletSelection: String?, walletType: String?, @@ -103,19 +102,20 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let result = try await client.wallet.completeEmailAuth( code: code, walletSelection: try self.walletSelectionBehavior(walletSelection), walletType: try self.walletType(walletType), sessionLifetimeSeconds: try self.sessionLifetimeSeconds(sessionLifetimeSeconds) ) - return try self.completeAuthResultDictionary(result) + return try self.completeAuthResultDictionary(clientId: clientId, result) } } - @objc(signInWithOidcIdTokenWithIdToken:issuer:audience:walletSelection:walletType:sessionLifetimeSeconds:resolve:reject:) + @objc(signInWithOidcIdTokenWithClientId:idToken:issuer:audience:walletSelection:walletType:sessionLifetimeSeconds:resolve:reject:) public func signInWithOidcIdToken( + clientId: String, idToken: String, issuer: String, audience: String, @@ -125,7 +125,7 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let result = try await client.wallet.signInWithOidcIdToken( idToken: idToken, issuer: issuer, @@ -134,12 +134,13 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { walletSelection: try self.walletSelectionBehavior(walletSelection), sessionLifetimeSeconds: try self.sessionLifetimeSeconds(sessionLifetimeSeconds) ) - return try self.completeAuthResultDictionary(result) + return try self.completeAuthResultDictionary(clientId: clientId, result) } } - @objc(startOidcRedirectAuthWithProviderJson:redirectUri:walletType:relayRedirectUri:authorizeParamsJson:loginHint:resolve:reject:) + @objc(startOidcRedirectAuthWithClientId:providerJson:redirectUri:walletType:relayRedirectUri:authorizeParamsJson:loginHint:resolve:reject:) public func startOidcRedirectAuth( + clientId: String, providerJson: String, redirectUri: String, walletType: String?, @@ -149,7 +150,7 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let result = try await client.wallet.startOidcRedirectAuth( provider: try self.decodeOidcProvider(providerJson), redirectUri: redirectUri, @@ -166,15 +167,16 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(handleOidcRedirectCallbackWithCallbackUrl:walletSelection:sessionLifetimeSeconds:resolve:reject:) + @objc(handleOidcRedirectCallbackWithClientId:callbackUrl:walletSelection:sessionLifetimeSeconds:resolve:reject:) public func handleOidcRedirectCallback( + clientId: String, callbackUrl: String?, walletSelection: String?, sessionLifetimeSeconds: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let result = try await client.wallet.handleOidcRedirectCallback( callbackUrl, walletSelection: try self.walletSelectionBehavior(walletSelection), @@ -182,7 +184,7 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { ) switch result { case .completed(let wallet): - self.clearPendingWalletSelections() + self.clearPendingWalletSelections(clientId: clientId) return [ "type": "completed", "wallet": self.walletDictionary(wallet) @@ -197,44 +199,47 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { "message": (error as NSError).localizedDescription ] case .walletSelection(let pendingSelection): - self.clearPendingWalletSelections() + self.clearPendingWalletSelections(clientId: clientId) return [ "type": "walletSelection", - "pendingSelection": self.pendingWalletSelectionDictionary(pendingSelection) + "pendingSelection": self.pendingWalletSelectionDictionary(clientId: clientId, pendingSelection) ] } } } - @objc(listWalletsWithResolve:reject:) + @objc(listWalletsWithClientId:resolve:reject:) public func listWallets( + clientId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in try await client.wallet.listWallets().map(self.walletDictionary) } } - @objc(useWalletWithWalletId:resolve:reject:) + @objc(useWalletWithClientId:walletId:resolve:reject:) public func useWallet( + clientId: String, walletId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in self.walletActivationResultDictionary(try await client.wallet.useWallet(walletId: walletId)) } } - @objc(createWalletWithWalletType:reference:resolve:reject:) + @objc(createWalletWithClientId:walletType:reference:resolve:reject:) public func createWallet( + clientId: String, walletType: String?, reference: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in self.walletActivationResultDictionary( try await client.wallet.createWallet( walletType: try self.walletType(walletType), @@ -244,15 +249,16 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(selectWalletForPendingSelectionWithPendingSelectionId:walletId:resolve:reject:) + @objc(selectWalletForPendingSelectionWithClientId:pendingSelectionId:walletId:resolve:reject:) public func selectWalletForPendingSelection( + clientId: String, pendingSelectionId: String, walletId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { do { - let pendingSelection = try requirePendingWalletSelection(pendingSelectionId) + let pendingSelection = try requirePendingWalletSelection(pendingSelectionId, clientId: clientId) let callbacks = PromiseCallbacks(resolve: resolve, reject: reject) Task { [callbacks, pendingSelection] in do { @@ -268,15 +274,16 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(createAndSelectWalletForPendingSelectionWithPendingSelectionId:reference:resolve:reject:) + @objc(createAndSelectWalletForPendingSelectionWithClientId:pendingSelectionId:reference:resolve:reject:) public func createAndSelectWalletForPendingSelection( + clientId: String, pendingSelectionId: String, reference: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { do { - let pendingSelection = try requirePendingWalletSelection(pendingSelectionId) + let pendingSelection = try requirePendingWalletSelection(pendingSelectionId, clientId: clientId) let callbacks = PromiseCallbacks(resolve: resolve, reject: reject) Task { [callbacks, pendingSelection] in do { @@ -292,41 +299,44 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(signOutWithResolve:reject:) + @objc(signOutWithClientId:resolve:reject:) public func signOut( + clientId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { do { - clearPendingWalletSelections() - try requireClient().wallet.signOut() + clearPendingWalletSelections(clientId: clientId) + try requireClient(clientId).wallet.signOut() resolve(nil) } catch { rejectError(reject, error) } } - @objc(signMessageWithChainId:message:resolve:reject:) + @objc(signMessageWithClientId:chainId:message:resolve:reject:) public func signMessage( + clientId: String, chainId: String, message: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let network = try self.requireNetwork(client, chainId: chainId) return try await client.wallet.signMessage(network: network, message: message) } } - @objc(signTypedDataWithChainId:typedDataJson:resolve:reject:) + @objc(signTypedDataWithClientId:chainId:typedDataJson:resolve:reject:) public func signTypedData( + clientId: String, chainId: String, typedDataJson: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let network = try self.requireNetwork(client, chainId: chainId) return try await client.wallet.signTypedData( network: network, @@ -335,8 +345,9 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(sendTransactionWithChainId:to:value:data:mode:feeOptionSelectorId:waitForStatus:statusPollingTimeoutMs:statusPollingIntervalMs:statusPollingFastIntervalMs:statusPollingFastPollCount:resolve:reject:) + @objc(sendTransactionWithClientId:chainId:to:value:data:mode:feeOptionSelectorId:waitForStatus:statusPollingTimeoutMs:statusPollingIntervalMs:statusPollingFastIntervalMs:statusPollingFastPollCount:resolve:reject:) public func sendTransaction( + clientId: String, chainId: String, to: String, value: String, @@ -351,7 +362,7 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let network = try self.requireNetwork(client, chainId: chainId) let request = SendTransactionRequest( to: to, @@ -366,31 +377,21 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { fastIntervalMs: statusPollingFastIntervalMs, fastPollCount: statusPollingFastPollCount ) - let result: SendTransactionResponse - - if waitForStatus && statusPolling == nil { - result = try await client.wallet.sendTransaction( - network: network, - request: request, - selectFeeOption: selectFeeOption - ) - } else { - result = try await self.sendTransactionWithStatusPolling( - client: client, + return self.sendTransactionResponseDictionary( + try await client.wallet.sendTransaction( network: network, request: request, selectFeeOption: selectFeeOption, waitForStatus: waitForStatus, - statusPolling: statusPolling ?? .defaultOptions + statusPolling: statusPolling ?? TransactionStatusPollingOptions() ) - } - - return self.sendTransactionResponseDictionary(result) + ) } } - @objc(callContractWithChainId:contractAddress:method:argsJson:mode:feeOptionSelectorId:waitForStatus:statusPollingTimeoutMs:statusPollingIntervalMs:statusPollingFastIntervalMs:statusPollingFastPollCount:resolve:reject:) + @objc(callContractWithClientId:chainId:contractAddress:method:argsJson:mode:feeOptionSelectorId:waitForStatus:statusPollingTimeoutMs:statusPollingIntervalMs:statusPollingFastIntervalMs:statusPollingFastPollCount:resolve:reject:) public func callContract( + clientId: String, chainId: String, contractAddress: String, method: String, @@ -405,7 +406,7 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let network = try self.requireNetwork(client, chainId: chainId) let args = try self.decodeAbiArgs(argsJson) let transactionMode = try self.transactionMode(mode) @@ -416,32 +417,18 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { fastIntervalMs: statusPollingFastIntervalMs, fastPollCount: statusPollingFastPollCount ) - let result: SendTransactionResponse - - if waitForStatus && statusPolling == nil { - result = try await client.wallet.callContract( + return self.sendTransactionResponseDictionary( + try await client.wallet.callContract( network: network, contract: contractAddress, method: method, args: args, selectFeeOption: selectFeeOption, - mode: transactionMode - ) - } else { - result = try await self.callContractWithStatusPolling( - client: client, - network: network, - contract: contractAddress, - method: method, - args: args, mode: transactionMode, - selectFeeOption: selectFeeOption, waitForStatus: waitForStatus, - statusPolling: statusPolling ?? .defaultOptions + statusPolling: statusPolling ?? TransactionStatusPollingOptions() ) - } - - return self.sendTransactionResponseDictionary(result) + ) } } @@ -466,74 +453,60 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { resolve(nil) } - @objc(getTransactionStatusWithTxnId:resolve:reject:) + @objc(getTransactionStatusWithClientId:txnId:resolve:reject:) public func getTransactionStatus( + clientId: String, txnId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in self.transactionStatusDictionary( try await client.wallet.getTransactionStatus(txnId: txnId) ) } } - @objc(getTokenBalancesWithChainId:contractAddress:walletAddress:includeMetadata:page:pageSize:resolve:reject:) - public func getTokenBalances( - chainId: String, - contractAddress: String?, - walletAddress: String, - includeMetadata: Bool, - page: String?, - pageSize: String?, + @objc(getBalancesWithClientId:paramsJson:resolve:reject:) + public func getBalances( + clientId: String, + paramsJson: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in - let network = try self.requireNetwork(client, chainId: chainId) - let result = try await client.indexer.getTokenBalances( - network: network, - contractAddress: contractAddress, - walletAddress: walletAddress, - includeMetadata: includeMetadata, - page: TokenBalancesPageRequest( - page: try self.uint32(page, name: "page").map { Int($0) }, - pageSize: try self.uint32(pageSize, name: "pageSize").map { Int($0) } - ) + run(clientId: clientId, resolve: resolve, reject: reject) { client in + let params = try self.decodeGetBalancesParams(paramsJson, client: client) + return self.tokenBalancesResultDictionary( + try await client.indexer.getBalances(params) ) - return self.tokenBalancesResultDictionary(result) } } - @objc(getNativeTokenBalanceWithChainId:walletAddress:resolve:reject:) - public func getNativeTokenBalance( - chainId: String, - walletAddress: String, + @objc(getTransactionHistoryWithClientId:paramsJson:resolve:reject:) + public func getTransactionHistory( + clientId: String, + paramsJson: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in - let network = try self.requireNetwork(client, chainId: chainId) - guard let balance = try await client.indexer.getNativeTokenBalance( - network: network, - walletAddress: walletAddress - ) else { - return NSNull() - } - return self.tokenBalanceDictionary(balance) + run(clientId: clientId, resolve: resolve, reject: reject) { client in + let params = try self.decodeGetTransactionHistoryParams(paramsJson, client: client) + return self.transactionHistoryResultDictionary( + try await client.indexer.getTransactionHistory(params) + ) } } - @objc(verifyMessageSignatureWithChainId:message:signature:resolve:reject:) + @objc(verifyMessageSignatureWithClientId:chainId:message:signature:resolve:reject:) public func verifyMessageSignature( + clientId: String, chainId: String, message: String, signature: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let network = try self.requireNetwork(client, chainId: chainId) return try await client.wallet.isValidMessageSignature( network: network, @@ -544,15 +517,16 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(verifyTypedDataSignatureWithChainId:typedDataJson:signature:resolve:reject:) + @objc(verifyTypedDataSignatureWithClientId:chainId:typedDataJson:signature:resolve:reject:) public func verifyTypedDataSignature( + clientId: String, chainId: String, typedDataJson: String, signature: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in let network = try self.requireNetwork(client, chainId: chainId) return try await client.wallet.isValidTypedDataSignature( network: network, @@ -563,14 +537,15 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(getIdTokenWithTtlSeconds:customClaimsJson:resolve:reject:) + @objc(getIdTokenWithClientId:ttlSeconds:customClaimsJson:resolve:reject:) public func getIdToken( + clientId: String, ttlSeconds: String?, customClaimsJson: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in try await client.wallet.getIdToken( ttlSeconds: try self.uint32(ttlSeconds, name: "ttlSeconds"), customClaims: try self.decodeCustomClaims(customClaimsJson) @@ -578,27 +553,29 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(listAccessWithPageSize:resolve:reject:) + @objc(listAccessWithClientId:pageSize:resolve:reject:) public func listAccess( + clientId: String, pageSize: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in try await client.wallet.listAccess( pageSize: try self.uint32(pageSize, name: "pageSize") ).map(self.credentialInfoDictionary) } } - @objc(listAccessPageWithPageSize:cursor:resolve:reject:) + @objc(listAccessPageWithClientId:pageSize:cursor:resolve:reject:) public func listAccessPage( + clientId: String, pageSize: String?, cursor: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in self.listAccessResponseDictionary( try await client.wallet.listAccessPage( pageSize: try self.uint32(pageSize, name: "pageSize"), @@ -608,13 +585,14 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - @objc(revokeAccessWithTargetCredentialId:resolve:reject:) + @objc(revokeAccessWithClientId:targetCredentialId:resolve:reject:) public func revokeAccess( + clientId: String, targetCredentialId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { - run(resolve: resolve, reject: reject) { client in + run(clientId: clientId, resolve: resolve, reject: reject) { client in try await client.wallet.revokeAccess(targetCredentialId: targetCredentialId) return nil } @@ -673,12 +651,13 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } private func run( + clientId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock, operation: @escaping @Sendable (OMSClient) async throws -> Any? ) { do { - let activeClient = ClientBox(try requireClient()) + let activeClient = ClientBox(try requireClient(clientId)) let callbacks = PromiseCallbacks(resolve: resolve, reject: reject) Task { [activeClient, callbacks, operation] in do { @@ -692,9 +671,18 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { } } - private func requireClient() throws -> OMSClient { + private func storeClient(_ client: OMSClient, clientId: String) { + clientsLock.lock() + clients[clientId] = client + clientsLock.unlock() + } + + private func requireClient(_ clientId: String) throws -> OMSClient { + clientsLock.lock() + let client = clients[clientId] + clientsLock.unlock() guard let client else { - throw makeError("Call configure before using the OMS client") + throw makeError("OMS client is not initialized: \(clientId)") } return client } @@ -706,370 +694,17 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { return network } - private func sendTransactionWithStatusPolling( - client: OMSClient, - network: Network, - request: SendTransactionRequest, - selectFeeOption: FeeOptionSelector?, - waitForStatus: Bool, - statusPolling: OmsBridgeTransactionStatusPollingOptions - ) async throws -> SendTransactionResponse { - let walletId = try requireActiveWalletId(client) - let signedClient = try signedWalletClient(client) - let prepared = try await signedClient.prepareEthereumTransaction( - PrepareEthereumTransactionRequest( - network: network.chainId, - walletId: walletId, - to: request.to, - value: request.value, - data: request.data, - mode: request.mode - ) - ) - - return try await executePreparedTransaction( - client: client, - signedClient: signedClient, - network: network, - prepared: prepared, - selectFeeOption: selectFeeOption, - waitForStatus: waitForStatus, - statusPolling: statusPolling - ) - } - - private func callContractWithStatusPolling( - client: OMSClient, - network: Network, - contract: String, - method: String, - args: [AbiArg]?, - mode: TransactionMode, - selectFeeOption: FeeOptionSelector?, - waitForStatus: Bool, - statusPolling: OmsBridgeTransactionStatusPollingOptions - ) async throws -> SendTransactionResponse { - let walletId = try requireActiveWalletId(client) - let signedClient = try signedWalletClient(client) - let prepared = try await signedClient.prepareEthereumContractCall( - PrepareEthereumContractCallRequest( - network: network.chainId, - walletId: walletId, - contract: contract, - method: method, - args: args, - mode: mode - ) - ) - - return try await executePreparedTransaction( - client: client, - signedClient: signedClient, - network: network, - prepared: prepared, - selectFeeOption: selectFeeOption, - waitForStatus: waitForStatus, - statusPolling: statusPolling - ) - } - - private func executePreparedTransaction( - client: OMSClient, - signedClient: WaasWalletClient, - network: Network, - prepared: PrepareResponse, - selectFeeOption: FeeOptionSelector?, - waitForStatus: Bool, - statusPolling: OmsBridgeTransactionStatusPollingOptions - ) async throws -> SendTransactionResponse { - let feeOption = try await chooseFeeOption( - client: client, - network: network, - prepared: prepared, - selectFeeOption: selectFeeOption - ) - let executed = try await signedClient.execute( - ExecuteRequest(txnId: prepared.txnId, feeOption: feeOption) - ) - - if !waitForStatus { - return SendTransactionResponse( - txnId: prepared.txnId, - status: executed.status - ) - } - - let status = try await waitForTransactionStatus( - signedClient: signedClient, - txnId: prepared.txnId, - fallbackStatus: executed.status, - options: statusPolling - ) - return SendTransactionResponse( - txnId: prepared.txnId, - status: status.status, - txnHash: status.txnHash - ) - } - - private func chooseFeeOption( - client: OMSClient, - network: Network, - prepared: PrepareResponse, - selectFeeOption: FeeOptionSelector? - ) async throws -> FeeOptionSelection? { - guard !prepared.sponsored else { - return nil - } - - guard !prepared.feeOptions.isEmpty else { - throw TransactionError.noFeeOptionsAvailable - } - - guard let selectFeeOption else { - guard let first = prepared.feeOptions.first else { - throw TransactionError.noFeeOptionsAvailable - } - return FeeOptionSelection(feeOption: first) - } - - let walletAddress = try requireActiveWalletAddress(client) - let selection = try await selectFeeOption( - enrichFeeOptionsWithBalances( - client: client, - network: network, - walletAddress: walletAddress, - feeOptions: prepared.feeOptions - ) - ) - - guard let selection else { - throw TransactionError.noFeeOptionSelected - } - - return selection - } - - private func enrichFeeOptionsWithBalances( - client: OMSClient, - network: Network, - walletAddress: String, - feeOptions: [FeeOption] - ) async -> [FeeOptionWithBalance] { - let nativeBalance: TokenBalance? - if feeOptions.contains(where: { isNativeFeeToken($0.token) }) { - nativeBalance = try? await client.indexer.getNativeTokenBalance( - network: network, - walletAddress: walletAddress - ) - } else { - nativeBalance = nil - } - - var balancesByContract: [String: TokenBalance?] = [:] - let contractAddresses = feeOptions - .compactMap { normalizedAddress($0.token.contractAddress) } - .reduce(into: [String]()) { addresses, address in - if !addresses.contains(address) { - addresses.append(address) - } - } - - for contractAddress in contractAddresses { - balancesByContract[contractAddress] = await loadTokenBalanceOrZero( - client: client, - network: network, - contractAddress: contractAddress, - walletAddress: walletAddress - ) - } - - return feeOptions.map { feeOption in - let balance: TokenBalance? - if isNativeFeeToken(feeOption.token) { - balance = nativeBalance - } else { - balance = normalizedAddress(feeOption.token.contractAddress) - .flatMap { balancesByContract[$0] ?? nil } - } - - let decimals = feeOption.token.decimals.map(Int.init) - return FeeOptionWithBalance( - feeOption: feeOption, - balance: balance, - available: formatTokenAmount(balance?.balance, decimals: decimals), - availableRaw: balance?.balance, - decimals: decimals - ) - } - } - - private func loadTokenBalanceOrZero( - client: OMSClient, - network: Network, - contractAddress: String, - walletAddress: String - ) async -> TokenBalance? { - do { - let result = try await client.indexer.getTokenBalances( - network: network, - contractAddress: contractAddress, - walletAddress: walletAddress, - includeMetadata: false - ) - return result.balances.first { - normalizedAddress($0.contractAddress) == contractAddress - } ?? TokenBalance( - contractType: "ERC20", - contractAddress: contractAddress, - accountAddress: walletAddress, - tokenId: nil, - balance: "0", - blockHash: nil, - blockNumber: nil, - chainId: Int64(network.chainId) - ) - } catch { - return nil - } - } - - private func waitForTransactionStatus( - signedClient: WaasWalletClient, - txnId: String, - fallbackStatus: TransactionStatus, - options: OmsBridgeTransactionStatusPollingOptions - ) async throws -> TransactionStatusResponse { - let deadline = Date().addingTimeInterval(TimeInterval(options.timeoutMs) / 1000) - var lastStatus = TransactionStatusResponse(status: fallbackStatus) - var completedPolls = 0 - - while true { - lastStatus = try await signedClient.transactionStatus( - TransactionStatusRequest(txnId: txnId) - ) - completedPolls += 1 - - if isSubmittedTransactionResult(lastStatus) { - return lastStatus - } - - let delayMs = transactionStatusPollDelayMs( - completedPolls: completedPolls, - options: options - ) - if delayMs <= 0 || Date() >= deadline { - return lastStatus - } - - let remainingMs = max(0, Int(deadline.timeIntervalSinceNow * 1000)) - let sleepMs = min(delayMs, remainingMs) - if sleepMs <= 0 { - return lastStatus - } - - try await Task.sleep(nanoseconds: UInt64(sleepMs) * 1_000_000) - } - } - - private func transactionStatusPollDelayMs( - completedPolls: Int, - options: OmsBridgeTransactionStatusPollingOptions - ) -> Int { - return completedPolls < options.fastPollCount - ? options.fastIntervalMs - : options.intervalMs - } - - private func signedWalletClient(_ client: OMSClient) throws -> WaasWalletClient { - // The Swift SDK does not expose waitForStatus/statusPolling knobs yet. - // Keep RN's API stable by reusing the SDK's signed WaaS client for the - // bridge-owned prepare/execute/polling path. - let mirror = Mirror(reflecting: client.wallet) - for child in mirror.children { - if (child.label == "signedClient" || child.label == "_signedClient"), - let signedClient = child.value as? WaasWalletClient { - return signedClient - } - } - throw makeError("Unable to access signed wallet client") - } - - private func requireActiveWalletId(_ client: OMSClient) throws -> String { - let walletId = client.wallet.walletId.trimmingCharacters(in: .whitespacesAndNewlines) - guard !walletId.isEmpty else { - throw makeError("No active wallet session") - } - return walletId - } - - private func isSubmittedTransactionResult(_ response: TransactionStatusResponse) -> Bool { - response.status == .executed || hasTransactionHash(response.txnHash) - } - - private func hasTransactionHash(_ txnHash: String?) -> Bool { - guard let txnHash = txnHash?.trimmingCharacters(in: .whitespacesAndNewlines) else { - return false - } - return !txnHash.isEmpty - } - - private func isNativeFeeToken(_ token: FeeToken) -> Bool { - token.type.caseInsensitiveCompare("native") == .orderedSame - || ((token.contractAddress?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) - && (token.tokenId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)) - } - - private func normalizedAddress(_ address: String?) -> String? { - guard let address = address?.trimmingCharacters(in: .whitespacesAndNewlines), !address.isEmpty else { - return nil - } - return address.lowercased() - } - - private func formatTokenAmount(_ rawAmount: String?, decimals: Int?) -> String? { - guard let rawAmount = rawAmount?.trimmingCharacters(in: .whitespacesAndNewlines), - !rawAmount.isEmpty else { - return nil - } - guard let decimals, decimals > 0 else { - return rawAmount - } - - let isNegative = rawAmount.hasPrefix("-") - let unsignedAmount = isNegative ? String(rawAmount.dropFirst()) : rawAmount - let paddedAmount = String( - repeating: "0", - count: max(0, decimals + 1 - unsignedAmount.count) - ) + unsignedAmount - let integerLength = paddedAmount.count - decimals - let integerEnd = paddedAmount.index(paddedAmount.startIndex, offsetBy: integerLength) - let integerPart = String(paddedAmount[.. String { - var result = value - while result.last == "0" { - result.removeLast() - } - return result - } - private func requireActiveWalletAddress(_ client: OMSClient) throws -> String { - let walletAddress = client.wallet.walletAddress - guard !walletAddress.isEmpty else { + guard let walletAddress = client.wallet.walletAddress?.nonEmpty else { throw makeError("No active wallet session") } return walletAddress } - private func completeAuthResultDictionary(_ result: CompleteAuthResult) throws -> [String: Any] { + private func completeAuthResultDictionary(clientId: String, _ result: CompleteAuthResult) throws -> [String: Any] { switch result { case .walletSelected(let walletAddress, let wallet, let wallets, let credential): - clearPendingWalletSelections() + clearPendingWalletSelections(clientId: clientId) return [ "type": "walletSelected", "walletAddress": walletAddress, @@ -1078,14 +713,14 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { "credential": credentialInfoDictionary(credential) ] case .walletSelection(let pendingSelection): - clearPendingWalletSelections() + clearPendingWalletSelections(clientId: clientId) return [ "type": "walletSelection", "walletAddress": NSNull(), "wallet": NSNull(), "wallets": pendingSelection.wallets.map(walletDictionary), "credential": credentialInfoDictionary(pendingSelection.credential), - "pendingSelection": pendingWalletSelectionDictionary(pendingSelection) + "pendingSelection": pendingWalletSelectionDictionary(clientId: clientId, pendingSelection) ] } } @@ -1099,10 +734,13 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { ] } - private func pendingWalletSelectionDictionary(_ pendingSelection: PendingWalletSelection) -> [String: Any] { + private func pendingWalletSelectionDictionary( + clientId: String, + _ pendingSelection: PendingWalletSelection + ) -> [String: Any] { let id = UUID().uuidString pendingWalletSelectionsLock.lock() - pendingWalletSelections[id] = pendingSelection + pendingWalletSelections[id] = StoredPendingWalletSelection(clientId: clientId, selection: pendingSelection) pendingWalletSelectionsLock.unlock() return [ "id": id, @@ -1119,15 +757,18 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { ] } - private func requirePendingWalletSelection(_ id: String) throws -> PendingWalletSelection { + private func requirePendingWalletSelection(_ id: String, clientId: String) throws -> PendingWalletSelection { pendingWalletSelectionsLock.lock() - let selection = pendingWalletSelections[id] + let storedSelection = pendingWalletSelections[id] pendingWalletSelectionsLock.unlock() - guard let selection else { + guard let storedSelection else { throw makeError("Pending wallet selection is no longer available") } - return selection + guard storedSelection.clientId == clientId else { + throw makeError("Pending wallet selection belongs to a different OMS client") + } + return storedSelection.selection } private func removePendingWalletSelection(_ id: String) { @@ -1136,9 +777,9 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { pendingWalletSelectionsLock.unlock() } - private func clearPendingWalletSelections() { + private func clearPendingWalletSelections(clientId: String) { pendingWalletSelectionsLock.lock() - pendingWalletSelections.removeAll() + pendingWalletSelections = pendingWalletSelections.filter { $0.value.clientId != clientId } pendingWalletSelectionsLock.unlock() } @@ -1151,8 +792,9 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { ] } - private func sessionExpiredEventDictionary(_ event: SessionExpiredEvent) -> [String: Any] { + private func sessionExpiredEventDictionary(clientId: String, event: SessionExpiredEvent) -> [String: Any] { [ + "clientId": clientId, "session": sessionDictionary(event.session), "expiredAt": iso8601String(event.expiredAt) ] @@ -1189,23 +831,33 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { ] } - private func tokenBalancesResultDictionary(_ result: TokenBalancesResult) -> [String: Any] { - var dictionary: [String: Any] = [ + private func tokenBalancesResultDictionary(_ result: BalancesResult) -> [String: Any] { + let dictionary: [String: Any] = [ "status": result.status, + "page": result.page.map(tokenBalancesPageDictionary) ?? NSNull(), + "nativeBalances": result.nativeBalances.map(tokenBalanceDictionary), "balances": result.balances.map(tokenBalanceDictionary) ] - if let page = result.page { - dictionary["page"] = [ - "page": page.page, - "pageSize": page.pageSize, - "more": page.more - ] - } - return dictionary } + private func transactionHistoryResultDictionary(_ result: TransactionHistoryResult) -> [String: Any] { + [ + "status": result.status, + "page": result.page.map(tokenBalancesPageDictionary) ?? NSNull(), + "transactions": result.transactions.map(transactionDictionary) + ] + } + + private func tokenBalancesPageDictionary(_ page: TokenBalancesPage) -> [String: Any] { + [ + "page": page.page.map(NSNumber.init(value:)) ?? NSNull(), + "pageSize": page.pageSize.map(NSNumber.init(value:)) ?? NSNull(), + "more": page.more.map(NSNumber.init(value:)) ?? NSNull() + ] + } + private func tokenBalanceDictionary(_ balance: TokenBalance) -> [String: Any] { var dictionary: [String: Any] = [:] dictionary["contractType"] = balance.contractType ?? NSNull() @@ -1213,6 +865,8 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { dictionary["accountAddress"] = balance.accountAddress ?? NSNull() dictionary["tokenId"] = balance.tokenId ?? NSNull() dictionary["balance"] = balance.balance ?? NSNull() + dictionary["name"] = balance.name ?? NSNull() + dictionary["symbol"] = balance.symbol ?? NSNull() dictionary["balanceUSD"] = balance.balanceUSD ?? NSNull() dictionary["priceUSD"] = balance.priceUSD ?? NSNull() dictionary["priceUpdatedAt"] = balance.priceUpdatedAt ?? NSNull() @@ -1245,6 +899,35 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { ] } + private func transactionDictionary(_ transaction: Transaction) -> [String: Any] { + [ + "txnHash": transaction.txnHash, + "blockNumber": NSNumber(value: transaction.blockNumber), + "blockHash": transaction.blockHash, + "chainId": NSNumber(value: transaction.chainId), + "metaTxnId": transaction.metaTxnId ?? NSNull(), + "transfers": transaction.transfers?.map(transactionTransferDictionary) ?? NSNull(), + "timestamp": transaction.timestamp + ] + } + + private func transactionTransferDictionary(_ transfer: TransactionTransfer) -> [String: Any] { + [ + "transferType": transfer.transferType ?? NSNull(), + "contractAddress": transfer.contractAddress ?? NSNull(), + "contractType": transfer.contractType ?? NSNull(), + "from": transfer.from ?? NSNull(), + "to": transfer.to ?? NSNull(), + "tokenIds": transfer.tokenIds ?? NSNull(), + "amounts": transfer.amounts ?? NSNull(), + "logIndex": transfer.logIndex.map(NSNumber.init(value:)) ?? NSNull(), + "amountsUSD": transfer.amountsUSD ?? NSNull(), + "pricesUSD": transfer.pricesUSD ?? NSNull(), + "contractInfo": transfer.contractInfo.map(tokenContractInfoDictionary) ?? NSNull(), + "tokenMetadata": transfer.tokenMetadata?.mapValues(tokenMetadataDictionary) ?? NSNull() + ] + } + private func tokenMetadataDictionary(_ metadata: TokenMetadata) -> [String: Any] { [ "chainId": metadata.chainId.map(NSNumber.init(value:)) ?? NSNull(), @@ -1334,7 +1017,7 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { "symbol": token.symbol, "type": token.type, "decimals": token.decimals.map(NSNumber.init(value:)) ?? NSNull(), - "logoUrl": token.logoUrl, + "logoUrl": token.logoUrl ?? NSNull(), "contractAddress": token.contractAddress ?? NSNull(), "tokenId": token.tokenId ?? NSNull() ] @@ -1417,7 +1100,7 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { intervalMs: String?, fastIntervalMs: String?, fastPollCount: String? - ) throws -> OmsBridgeTransactionStatusPollingOptions? { + ) throws -> TransactionStatusPollingOptions? { guard timeoutMs != nil || intervalMs != nil || fastIntervalMs != nil @@ -1425,10 +1108,10 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { return nil } - return OmsBridgeTransactionStatusPollingOptions( + return TransactionStatusPollingOptions( timeoutMs: UInt64(try uint32(timeoutMs, name: "statusPolling.timeoutMs") ?? 60_000), - intervalMs: Int(try uint32(intervalMs, name: "statusPolling.intervalMs") ?? 2_000), - fastIntervalMs: Int(try uint32(fastIntervalMs, name: "statusPolling.fastIntervalMs") ?? 400), + intervalMs: UInt64(try uint32(intervalMs, name: "statusPolling.intervalMs") ?? 2_000), + fastIntervalMs: UInt64(try uint32(fastIntervalMs, name: "statusPolling.fastIntervalMs") ?? 400), fastPollCount: Int(try uint32(fastPollCount, name: "statusPolling.fastPollCount") ?? 5) ) } @@ -1460,6 +1143,61 @@ public final class OmsClientReactNativeSdkImpl: NSObject, @unchecked Sendable { return try JSONDecoder().decode([String: String].self, from: data) } + private func decodeGetBalancesParams(_ jsonString: String, client: OMSClient) throws -> GetBalancesParams { + let value = try decodeJSON(SerializableGetBalancesParams.self, from: jsonString, name: "params") + return GetBalancesParams( + walletAddress: value.walletAddress, + networks: try networks(value.networks, client: client), + networkType: value.networkType, + contractAddresses: value.contractAddresses, + includeMetadata: value.includeMetadata ?? true, + omitPrices: value.omitPrices, + tokenIds: value.tokenIds, + contractStatus: value.contractStatus, + page: value.page?.tokenBalancesPageRequest + ) + } + + private func decodeGetTransactionHistoryParams( + _ jsonString: String, + client: OMSClient + ) throws -> GetTransactionHistoryParams { + let value = try decodeJSON( + SerializableGetTransactionHistoryParams.self, + from: jsonString, + name: "params" + ) + return GetTransactionHistoryParams( + walletAddress: value.walletAddress, + networks: try networks(value.networks, client: client), + networkType: value.networkType, + contractAddresses: value.contractAddresses, + transactionHashes: value.transactionHashes, + metaTransactionIds: value.metaTransactionIds, + fromBlock: value.fromBlock, + toBlock: value.toBlock, + tokenId: value.tokenId, + includeMetadata: value.includeMetadata ?? true, + omitPrices: value.omitPrices, + metadataOptions: value.metadataOptions, + page: value.page?.tokenBalancesPageRequest + ) + } + + private func decodeJSON(_ type: T.Type, from jsonString: String, name: String) throws -> T { + guard let data = jsonString.data(using: .utf8) else { + throw makeError("\(name) must be UTF-8 JSON") + } + return try JSONDecoder().decode(type, from: data) + } + + private func networks(_ chainIds: [String]?, client: OMSClient) throws -> [Network]? { + guard let chainIds, !chainIds.isEmpty else { + return nil + } + return try chainIds.map { try requireNetwork(client, chainId: $0) } + } + private func decodeOidcProvider(_ jsonString: String) throws -> OidcProviderConfig { guard let data = jsonString.data(using: .utf8) else { throw makeError("provider must be UTF-8 JSON") @@ -1569,18 +1307,46 @@ private final class PromiseCallbacks: @unchecked Sendable { } } -private struct OmsBridgeTransactionStatusPollingOptions: Sendable { - static let defaultOptions = OmsBridgeTransactionStatusPollingOptions( - timeoutMs: 60_000, - intervalMs: 2_000, - fastIntervalMs: 400, - fastPollCount: 5 - ) - - let timeoutMs: UInt64 - let intervalMs: Int - let fastIntervalMs: Int - let fastPollCount: Int +private struct StoredPendingWalletSelection { + let clientId: String + let selection: PendingWalletSelection +} + +private struct SerializableTokenBalancesPageRequest: Decodable { + let page: Int? + let pageSize: Int? + + var tokenBalancesPageRequest: TokenBalancesPageRequest { + TokenBalancesPageRequest(page: page, pageSize: pageSize) + } +} + +private struct SerializableGetBalancesParams: Decodable { + let walletAddress: String + let networks: [String]? + let networkType: IndexerNetworkType? + let contractAddresses: [String]? + let includeMetadata: Bool? + let omitPrices: Bool? + let tokenIds: [String]? + let contractStatus: ContractVerificationStatus? + let page: SerializableTokenBalancesPageRequest? +} + +private struct SerializableGetTransactionHistoryParams: Decodable { + let walletAddress: String + let networks: [String]? + let networkType: IndexerNetworkType? + let contractAddresses: [String]? + let transactionHashes: [String]? + let metaTransactionIds: [String]? + let fromBlock: Int? + let toBlock: Int? + let tokenId: String? + let includeMetadata: Bool? + let omitPrices: Bool? + let metadataOptions: MetadataOptions? + let page: SerializableTokenBalancesPageRequest? } private struct SerializableOidcProviderConfig: Decodable { @@ -1591,3 +1357,10 @@ private struct SerializableOidcProviderConfig: Decodable { let relayRedirectUri: String? let authorizeParams: [String: String]? } + +private extension String { + var nonEmpty: String? { + let trimmed = trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } +} diff --git a/package.json b/package.json index eb4ad02..23502b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0xsequence/oms-react-native-sdk", - "version": "0.1.0-alpha.2", + "version": "0.1.0-alpha.3", "description": "React Native SDK for the OMS platform.", "homepage": "https://github.com/0xsequence/react-native-sdk", "main": "./lib/commonjs/index.js", diff --git a/src/NativeOmsClientReactNativeSdk.ts b/src/NativeOmsClientReactNativeSdk.ts index a502078..6963d10 100644 --- a/src/NativeOmsClientReactNativeSdk.ts +++ b/src/NativeOmsClientReactNativeSdk.ts @@ -60,10 +60,16 @@ export type OmsClientSessionState = { sessionEmail: string | null; }; +export type OmsClientSessionExpiredEvent = { + clientId: string; + session: OmsClientSessionState; + expiredAt: string; +}; + export type OmsTokenBalancesPage = { - page: number; - pageSize: number; - more: boolean; + page: number | null; + pageSize: number | null; + more: boolean | null; }; export type OmsTokenBalance = { @@ -72,12 +78,14 @@ export type OmsTokenBalance = { accountAddress: string | null; tokenId: string | null; balance: string | null; - balanceUSD?: string | null; - priceUSD?: string | null; - priceUpdatedAt?: string | null; blockHash: string | null; blockNumber?: number | null; chainId?: number | null; + name?: string | null; + symbol?: string | null; + balanceUSD?: string | null; + priceUSD?: string | null; + priceUpdatedAt?: string | null; uniqueCollectibles?: string | null; isSummary?: boolean | null; contractInfo?: OmsTokenContractInfo | null; @@ -139,12 +147,44 @@ export type OmsTokenMetadata = { lastFetched?: string | null; }; -export type OmsTokenBalancesResult = { +export type OmsBalancesResult = { status: number; - page?: OmsTokenBalancesPage; + page?: OmsTokenBalancesPage | null; + nativeBalances: OmsTokenBalance[]; balances: OmsTokenBalance[]; }; +export type OmsTransactionTransfer = { + transferType?: string | null; + contractAddress?: string | null; + contractType?: string | null; + from?: string | null; + to?: string | null; + tokenIds?: string[] | null; + amounts?: string[] | null; + logIndex?: number | null; + amountsUSD?: string[] | null; + pricesUSD?: string[] | null; + contractInfo?: OmsTokenContractInfo | null; + tokenMetadata?: CodegenTypes.UnsafeObject | null; +}; + +export type OmsTransaction = { + txnHash: string | null; + blockNumber: number | null; + blockHash: string | null; + chainId: number | null; + metaTxnId?: string | null; + transfers?: OmsTransactionTransfer[] | null; + timestamp?: string | null; +}; + +export type OmsTransactionHistoryResult = { + status: number; + page?: OmsTokenBalancesPage | null; + transactions: OmsTransaction[]; +}; + export type OmsTransactionStatus = { status: string; txnHash: string | null; @@ -162,7 +202,7 @@ export type OmsFeeToken = { symbol: string; type: string; decimals: number | null; - logoUrl: string; + logoUrl: string | null; contractAddress: string | null; tokenId: string | null; }; @@ -198,11 +238,6 @@ export type OmsCredentialInfo = { isCaller: boolean; }; -export type OmsClientSessionExpiredEvent = { - session: OmsClientSessionState; - expiredAt: string; -}; - export type OmsAccessPage = { limit: number | null; cursor: string | null; @@ -216,24 +251,20 @@ export type OmsListAccessResponse = { export interface Spec extends TurboModule { readonly onFeeOptionSelectionRequest: CodegenTypes.EventEmitter; readonly onSessionExpired: CodegenTypes.EventEmitter; - configure( - publishableKey: string, - walletApiUrl: string | null, - apiRpcUrl: string | null, - indexerUrlTemplate: string | null, - projectId: string - ): Promise; - getWalletAddress(): Promise; - getSession(): Promise; - getSupportedNetworks(): Promise; - startEmailAuth(email: string): Promise; + + createClient(clientId: string, publishableKey: string): Promise; + getWalletAddress(clientId: string): Promise; + getSession(clientId: string): Promise; + startEmailAuth(clientId: string, email: string): Promise; completeEmailAuth( + clientId: string, code: string, walletSelection: string | null, walletType: string | null, sessionLifetimeSeconds: string | null ): Promise; signInWithOidcIdToken( + clientId: string, idToken: string, issuer: string, audience: string, @@ -242,6 +273,7 @@ export interface Spec extends TurboModule { sessionLifetimeSeconds: string | null ): Promise; startOidcRedirectAuth( + clientId: string, providerJson: string, redirectUri: string, walletType: string | null, @@ -250,28 +282,44 @@ export interface Spec extends TurboModule { loginHint: string | null ): Promise; handleOidcRedirectCallback( + clientId: string, callbackUrl: string | null, walletSelection: string | null, sessionLifetimeSeconds: string | null ): Promise; - listWallets(): Promise; - useWallet(walletId: string): Promise; + listWallets(clientId: string): Promise; + useWallet( + clientId: string, + walletId: string + ): Promise; createWallet( + clientId: string, walletType: string | null, reference: string | null ): Promise; selectWalletForPendingSelection( + clientId: string, pendingSelectionId: string, walletId: string ): Promise; createAndSelectWalletForPendingSelection( + clientId: string, pendingSelectionId: string, reference: string | null ): Promise; - signOut(): Promise; - signMessage(chainId: string, message: string): Promise; - signTypedData(chainId: string, typedDataJson: string): Promise; + signOut(clientId: string): Promise; + signMessage( + clientId: string, + chainId: string, + message: string + ): Promise; + signTypedData( + clientId: string, + chainId: string, + typedDataJson: string + ): Promise; sendTransaction( + clientId: string, chainId: string, to: string, value: string, @@ -285,6 +333,7 @@ export interface Spec extends TurboModule { statusPollingFastPollCount: string | null ): Promise; callContract( + clientId: string, chainId: string, contractAddress: string, method: string, @@ -302,39 +351,42 @@ export interface Spec extends TurboModule { selectionToken: string | null, errorMessage: string | null ): Promise; - getTransactionStatus(txnId: string): Promise; - getTokenBalances( - chainId: string, - contractAddress: string | null, - walletAddress: string, - includeMetadata: boolean, - page: string | null, - pageSize: string | null - ): Promise; - getNativeTokenBalance( - chainId: string, - walletAddress: string - ): Promise; + getTransactionStatus( + clientId: string, + txnId: string + ): Promise; + getBalances(clientId: string, paramsJson: string): Promise; + getTransactionHistory( + clientId: string, + paramsJson: string + ): Promise; verifyMessageSignature( + clientId: string, chainId: string, message: string, signature: string ): Promise; verifyTypedDataSignature( + clientId: string, chainId: string, typedDataJson: string, signature: string ): Promise; getIdToken( + clientId: string, ttlSeconds: string | null, customClaimsJson: string | null ): Promise; - listAccess(pageSize: string | null): Promise; + listAccess( + clientId: string, + pageSize: string | null + ): Promise; listAccessPage( + clientId: string, pageSize: string | null, cursor: string | null ): Promise; - revokeAccess(targetCredentialId: string): Promise; + revokeAccess(clientId: string, targetCredentialId: string): Promise; } export default TurboModuleRegistry.getEnforcing( diff --git a/src/client.native.ts b/src/client.native.ts index b8e00dd..f20d437 100644 --- a/src/client.native.ts +++ b/src/client.native.ts @@ -7,17 +7,19 @@ import type { OmsNativeOidcRedirectAuthResult, OmsNativePendingWalletSelection, } from './NativeOmsClientReactNativeSdk'; +import { supportedNetworks } from './networks'; import type { CallContractParams, CompleteEmailAuthParams, CreateWalletParams, - HandleOidcRedirectCallbackParams, + GetBalancesParams, GetIdTokenParams, - GetNativeTokenBalanceParams, - GetTokenBalancesParams, + GetTransactionHistoryParams, + HandleOidcRedirectCallbackParams, ListAccessPageParams, ListAccessPagesParams, ListAccessParams, + OmsBalancesResult, OmsClientConfig, OmsClientSessionExpiredEvent, OmsClientSessionState, @@ -30,8 +32,7 @@ import type { OmsPendingWalletSelection, OmsSendTransactionResponse, OmsStartOidcRedirectAuthResult, - OmsTokenBalance, - OmsTokenBalancesResult, + OmsTransactionHistoryResult, OmsTransactionStatus, OmsWallet, OmsWalletActivationResult, @@ -43,6 +44,10 @@ import type { VerifyTypedDataSignatureParams, } from './types'; +type IndexerParamsWithNetworks = + | GetBalancesParams + | GetTransactionHistoryParams; + function stringifyRequiredJson(value: unknown, name: string): string { const json = JSON.stringify(value); if (json == null) { @@ -79,7 +84,121 @@ function resolveRelayRedirectUri( return params.provider.relayRedirectUri ?? null; } +function serializeIndexerParams(params: IndexerParamsWithNetworks): string { + return stringifyRequiredJson( + { + ...params, + networks: params.networks?.map((network) => network.chainId), + }, + 'params' + ); +} + +function requireNativeField(value: T | null | undefined, name: string): T { + if (value == null) { + throw new Error(`Native auth result is missing ${name}`); + } + return value; +} + +let nextClientId = 0; +let nextFeeOptionSelectorId = 0; +let feeOptionSelectionSubscription: EventSubscription | null = null; +const feeOptionSelectors = new Map(); +let sessionExpiredSubscription: EventSubscription | null = null; +const latestSessionExpiredEvents = new Map< + string, + OmsClientSessionExpiredEvent +>(); +const sessionExpiredListeners = new Map< + string, + Set<(event: OmsClientSessionExpiredEvent) => void> +>(); +const activateNativeWallet = OmsClientReactNativeSdk.useWallet.bind( + OmsClientReactNativeSdk +); + +function ensureFeeOptionSelectionListener() { + feeOptionSelectionSubscription ??= + OmsClientReactNativeSdk.onFeeOptionSelectionRequest( + handleFeeOptionSelectionRequest + ); +} + +function handleNativeSessionExpired(event: OmsNativeClientSessionExpiredEvent) { + const sessionExpiredEvent: OmsClientSessionExpiredEvent = { + session: event.session as OmsClientSessionState, + expiredAt: event.expiredAt, + }; + latestSessionExpiredEvents.set(event.clientId, sessionExpiredEvent); + const listeners = sessionExpiredListeners.get(event.clientId); + if (listeners == null) { + return; + } + for (const listener of Array.from(listeners)) { + listener(sessionExpiredEvent); + } +} + +function ensureSessionExpiredListener() { + sessionExpiredSubscription ??= OmsClientReactNativeSdk.onSessionExpired( + handleNativeSessionExpired + ); +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +async function handleFeeOptionSelectionRequest( + event: OmsFeeOptionSelectionRequest +) { + const selector = feeOptionSelectors.get(event.selectorId); + if (selector == null) { + await OmsClientReactNativeSdk.respondToFeeOptionSelection( + event.requestId, + null, + `Fee option selector ${event.selectorId} is no longer registered` + ); + return; + } + + try { + const selection = await selector(event.options); + await OmsClientReactNativeSdk.respondToFeeOptionSelection( + event.requestId, + selection?.token ?? null, + null + ); + } catch (error) { + await OmsClientReactNativeSdk.respondToFeeOptionSelection( + event.requestId, + null, + errorMessage(error) + ); + } +} + +async function withFeeOptionSelector( + selector: OmsFeeOptionSelector | null | undefined, + operation: (selectorId: string | null) => Promise +): Promise { + if (selector == null) { + return operation(null); + } + + ensureFeeOptionSelectionListener(); + const selectorId = `fee-option-selector-${++nextFeeOptionSelectorId}`; + feeOptionSelectors.set(selectorId, selector); + try { + return await operation(selectorId); + } finally { + feeOptionSelectors.delete(selectorId); + } +} + function hydratePendingWalletSelection( + owner: OMSClient, pendingSelection: OmsNativePendingWalletSelection ): OmsPendingWalletSelection { return { @@ -87,34 +206,32 @@ function hydratePendingWalletSelection( walletType: pendingSelection.walletType as OmsPendingWalletSelection['walletType'], async selectWallet(walletId: string) { + await owner.ensureReady(); const result = await OmsClientReactNativeSdk.selectWalletForPendingSelection( + owner.clientId, pendingSelection.id, walletId ); - resetSessionExpiredReplay(); + owner.resetSessionExpiredReplay(); return result; }, async createAndSelectWallet(reference?: string | null) { + await owner.ensureReady(); const result = await OmsClientReactNativeSdk.createAndSelectWalletForPendingSelection( + owner.clientId, pendingSelection.id, reference ?? null ); - resetSessionExpiredReplay(); + owner.resetSessionExpiredReplay(); return result; }, }; } -function requireNativeField(value: T | null | undefined, name: string): T { - if (value == null) { - throw new Error(`Native auth result is missing ${name}`); - } - return value; -} - function hydrateCompleteAuthResult( + owner: OMSClient, result: OmsNativeCompleteAuthResult ): OmsCompleteAuthResult { switch (result.type) { @@ -134,6 +251,7 @@ function hydrateCompleteAuthResult( } case 'walletSelection': { const pendingSelection = hydratePendingWalletSelection( + owner, requireNativeField(result.pendingSelection, 'pendingSelection') ); return { @@ -151,6 +269,7 @@ function hydrateCompleteAuthResult( } function hydrateOidcRedirectAuthResult( + owner: OMSClient, result: OmsNativeOidcRedirectAuthResult ): OmsOidcRedirectAuthResult { switch (result.type) { @@ -163,6 +282,7 @@ function hydrateOidcRedirectAuthResult( return { type: 'walletSelection', pendingSelection: hydratePendingWalletSelection( + owner, requireNativeField(result.pendingSelection, 'pendingSelection') ), }; @@ -181,368 +301,354 @@ function hydrateOidcRedirectAuthResult( } } -let nextFeeOptionSelectorId = 0; -let feeOptionSelectionSubscription: EventSubscription | null = null; -const feeOptionSelectors = new Map(); -let sessionExpiredSubscription: EventSubscription | null = null; -let latestSessionExpiredEvent: OmsClientSessionExpiredEvent | null = null; -const sessionExpiredListeners = new Set< - (event: OmsClientSessionExpiredEvent) => void ->(); +export class OMSClient { + public readonly wallet: OMSWalletClient; + public readonly indexer: OMSIndexerClient; + public readonly supportedNetworks: OmsNetwork[] = supportedNetworks; + public readonly clientId: string; + private readonly ready: Promise; -function ensureFeeOptionSelectionListener() { - feeOptionSelectionSubscription ??= - OmsClientReactNativeSdk.onFeeOptionSelectionRequest( - handleFeeOptionSelectionRequest + constructor(config: OmsClientConfig) { + ensureSessionExpiredListener(); + this.clientId = `oms-client-${++nextClientId}`; + this.ready = OmsClientReactNativeSdk.createClient( + this.clientId, + config.publishableKey ); -} + this.wallet = new OMSWalletClient(this); + this.indexer = new OMSIndexerClient(this); + } -function resetSessionExpiredReplay() { - latestSessionExpiredEvent = null; -} + public ensureReady(): Promise { + return this.ready; + } -function handleNativeSessionExpired(event: OmsNativeClientSessionExpiredEvent) { - const sessionExpiredEvent = event as OmsClientSessionExpiredEvent; - latestSessionExpiredEvent = sessionExpiredEvent; - for (const listener of Array.from(sessionExpiredListeners)) { - listener(sessionExpiredEvent); + public resetSessionExpiredReplay() { + latestSessionExpiredEvents.delete(this.clientId); } } -function ensureSessionExpiredListener() { - sessionExpiredSubscription ??= OmsClientReactNativeSdk.onSessionExpired( - handleNativeSessionExpired - ); -} +export class OMSWalletClient { + constructor(private readonly owner: OMSClient) {} -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} + async getWalletAddress(): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.getWalletAddress(this.owner.clientId); + } -async function handleFeeOptionSelectionRequest( - event: OmsFeeOptionSelectionRequest -) { - const selector = feeOptionSelectors.get(event.selectorId); - if (selector == null) { - await OmsClientReactNativeSdk.respondToFeeOptionSelection( - event.requestId, - null, - `Fee option selector ${event.selectorId} is no longer registered` - ); - return; + async getSession(): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.getSession( + this.owner.clientId + ) as Promise; } - try { - const selection = await selector(event.options); - await OmsClientReactNativeSdk.respondToFeeOptionSelection( - event.requestId, - selection?.token ?? null, - null - ); - } catch (error) { - await OmsClientReactNativeSdk.respondToFeeOptionSelection( - event.requestId, - null, - errorMessage(error) + onSessionExpired( + listener: (event: OmsClientSessionExpiredEvent) => void + ): EventSubscription { + ensureSessionExpiredListener(); + let listeners = sessionExpiredListeners.get(this.owner.clientId); + if (listeners == null) { + listeners = new Set(); + sessionExpiredListeners.set(this.owner.clientId, listeners); + } + listeners.add(listener); + + const latestSessionExpiredEvent = latestSessionExpiredEvents.get( + this.owner.clientId ); - } -} + if (latestSessionExpiredEvent != null) { + listener(latestSessionExpiredEvent); + } -async function withFeeOptionSelector( - selector: OmsFeeOptionSelector | null | undefined, - operation: (selectorId: string | null) => Promise -): Promise { - if (selector == null) { - return operation(null); + return { + remove: () => { + listeners.delete(listener); + if (listeners.size === 0) { + sessionExpiredListeners.delete(this.owner.clientId); + } + }, + }; } - ensureFeeOptionSelectionListener(); - const selectorId = `fee-option-selector-${++nextFeeOptionSelectorId}`; - feeOptionSelectors.set(selectorId, selector); - try { - return await operation(selectorId); - } finally { - feeOptionSelectors.delete(selectorId); + async startEmailAuth(email: string): Promise { + await this.owner.ensureReady(); + this.owner.resetSessionExpiredReplay(); + return OmsClientReactNativeSdk.startEmailAuth(this.owner.clientId, email); } -} -export function configure(config: OmsClientConfig): Promise { - resetSessionExpiredReplay(); - ensureSessionExpiredListener(); - return OmsClientReactNativeSdk.configure( - config.publishableKey, - config.environment?.walletApiUrl ?? null, - config.environment?.apiRpcUrl ?? null, - config.environment?.indexerUrlTemplate ?? null, - config.projectId - ); -} - -export function getWalletAddress(): Promise { - return OmsClientReactNativeSdk.getWalletAddress(); -} - -export function getSession(): Promise { - return OmsClientReactNativeSdk.getSession() as Promise; -} - -export function onSessionExpired( - listener: (event: OmsClientSessionExpiredEvent) => void -): EventSubscription { - ensureSessionExpiredListener(); - sessionExpiredListeners.add(listener); + async completeEmailAuth( + params: CompleteEmailAuthParams + ): Promise { + await this.owner.ensureReady(); + const result = hydrateCompleteAuthResult( + this.owner, + await OmsClientReactNativeSdk.completeEmailAuth( + this.owner.clientId, + params.code, + params.walletSelection ?? null, + params.walletType ?? null, + stringifyOptionalNumber(params.sessionLifetimeSeconds) + ) + ); + this.owner.resetSessionExpiredReplay(); + return result; + } - if (latestSessionExpiredEvent != null) { - listener(latestSessionExpiredEvent); + async signInWithOidcIdToken( + params: SignInWithOidcIdTokenParams + ): Promise { + await this.owner.ensureReady(); + this.owner.resetSessionExpiredReplay(); + return hydrateCompleteAuthResult( + this.owner, + await OmsClientReactNativeSdk.signInWithOidcIdToken( + this.owner.clientId, + params.idToken, + params.issuer, + params.audience, + params.walletSelection ?? null, + params.walletType ?? null, + stringifyOptionalNumber(params.sessionLifetimeSeconds) + ) + ); } - return { - remove() { - sessionExpiredListeners.delete(listener); - }, - }; -} + async startOidcRedirectAuth( + params: StartOidcRedirectAuthParams + ): Promise { + await this.owner.ensureReady(); + this.owner.resetSessionExpiredReplay(); + return OmsClientReactNativeSdk.startOidcRedirectAuth( + this.owner.clientId, + stringifyRequiredJson(params.provider, 'provider'), + params.redirectUri, + params.walletType ?? null, + resolveRelayRedirectUri(params), + stringifyOptionalJson(params.authorizeParams), + params.loginHint ?? null + ); + } -export function getSupportedNetworks(): Promise { - return OmsClientReactNativeSdk.getSupportedNetworks(); -} + async handleOidcRedirectCallback( + params: HandleOidcRedirectCallbackParams = {} + ): Promise { + await this.owner.ensureReady(); + const result = hydrateOidcRedirectAuthResult( + this.owner, + await OmsClientReactNativeSdk.handleOidcRedirectCallback( + this.owner.clientId, + params.callbackUrl ?? null, + params.walletSelection ?? null, + stringifyOptionalNumber(params.sessionLifetimeSeconds) + ) + ); + if ( + result.type !== 'notOidcRedirectCallback' && + result.type !== 'noPendingAuth' + ) { + this.owner.resetSessionExpiredReplay(); + } + return result; + } -export function startEmailAuth(email: string): Promise { - resetSessionExpiredReplay(); - return OmsClientReactNativeSdk.startEmailAuth(email); -} + async listWallets(): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.listWallets(this.owner.clientId); + } -export async function completeEmailAuth( - params: CompleteEmailAuthParams -): Promise { - const result = hydrateCompleteAuthResult( - await OmsClientReactNativeSdk.completeEmailAuth( - params.code, - params.walletSelection ?? null, - params.walletType ?? null, - stringifyOptionalNumber(params.sessionLifetimeSeconds) - ) - ); - resetSessionExpiredReplay(); - return result; -} + async useWallet(walletId: string): Promise { + await this.owner.ensureReady(); + const result = await activateNativeWallet(this.owner.clientId, walletId); + this.owner.resetSessionExpiredReplay(); + return result; + } -export async function signInWithOidcIdToken( - params: SignInWithOidcIdTokenParams -): Promise { - resetSessionExpiredReplay(); - return hydrateCompleteAuthResult( - await OmsClientReactNativeSdk.signInWithOidcIdToken( - params.idToken, - params.issuer, - params.audience, - params.walletSelection ?? null, + async createWallet( + params: CreateWalletParams = {} + ): Promise { + await this.owner.ensureReady(); + const result = await OmsClientReactNativeSdk.createWallet( + this.owner.clientId, params.walletType ?? null, - stringifyOptionalNumber(params.sessionLifetimeSeconds) - ) - ); -} - -export function startOidcRedirectAuth( - params: StartOidcRedirectAuthParams -): Promise { - resetSessionExpiredReplay(); - return OmsClientReactNativeSdk.startOidcRedirectAuth( - stringifyRequiredJson(params.provider, 'provider'), - params.redirectUri, - params.walletType ?? null, - resolveRelayRedirectUri(params), - stringifyOptionalJson(params.authorizeParams), - params.loginHint ?? null - ); -} - -export async function handleOidcRedirectCallback( - params: HandleOidcRedirectCallbackParams = {} -): Promise { - const result = hydrateOidcRedirectAuthResult( - await OmsClientReactNativeSdk.handleOidcRedirectCallback( - params.callbackUrl ?? null, - params.walletSelection ?? null, - stringifyOptionalNumber(params.sessionLifetimeSeconds) - ) - ); - if ( - result.type !== 'notOidcRedirectCallback' && - result.type !== 'noPendingAuth' - ) { - resetSessionExpiredReplay(); + params.reference ?? null + ); + this.owner.resetSessionExpiredReplay(); + return result; } - return result; -} -export function listWallets(): Promise { - return OmsClientReactNativeSdk.listWallets(); -} + async signOut(): Promise { + await this.owner.ensureReady(); + this.owner.resetSessionExpiredReplay(); + return OmsClientReactNativeSdk.signOut(this.owner.clientId); + } -export function useWallet( - walletId: string -): Promise { - return OmsClientReactNativeSdk.useWallet(walletId).then((result) => { - resetSessionExpiredReplay(); - return result; - }); -} + async signMessage(chainId: string, message: string): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.signMessage( + this.owner.clientId, + chainId, + message + ); + } -export async function createWallet( - params: CreateWalletParams = {} -): Promise { - const result = await OmsClientReactNativeSdk.createWallet( - params.walletType ?? null, - params.reference ?? null - ); - resetSessionExpiredReplay(); - return result; -} + async signTypedData(params: SignTypedDataParams): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.signTypedData( + this.owner.clientId, + params.chainId, + stringifyRequiredJson(params.typedData, 'typedData') + ); + } -export function signOut(): Promise { - resetSessionExpiredReplay(); - return OmsClientReactNativeSdk.signOut(); -} + async sendTransaction( + params: SendTransactionParams + ): Promise { + await this.owner.ensureReady(); + return withFeeOptionSelector(params.selectFeeOption, (selectorId) => + OmsClientReactNativeSdk.sendTransaction( + this.owner.clientId, + params.chainId, + params.to, + params.value, + params.data ?? null, + params.mode ?? null, + selectorId, + params.waitForStatus ?? true, + stringifyOptionalNumber(params.statusPolling?.timeoutMs), + stringifyOptionalNumber(params.statusPolling?.intervalMs), + stringifyOptionalNumber(params.statusPolling?.fastIntervalMs), + stringifyOptionalNumber(params.statusPolling?.fastPollCount) + ) + ); + } -export function signMessage(chainId: string, message: string): Promise { - return OmsClientReactNativeSdk.signMessage(chainId, message); -} + async callContract( + params: CallContractParams + ): Promise { + await this.owner.ensureReady(); + return withFeeOptionSelector(params.selectFeeOption, (selectorId) => + OmsClientReactNativeSdk.callContract( + this.owner.clientId, + params.chainId, + params.contractAddress, + params.method, + stringifyOptionalJson(params.args), + params.mode ?? null, + selectorId, + params.waitForStatus ?? true, + stringifyOptionalNumber(params.statusPolling?.timeoutMs), + stringifyOptionalNumber(params.statusPolling?.intervalMs), + stringifyOptionalNumber(params.statusPolling?.fastIntervalMs), + stringifyOptionalNumber(params.statusPolling?.fastPollCount) + ) + ); + } -export function signTypedData(params: SignTypedDataParams): Promise { - return OmsClientReactNativeSdk.signTypedData( - params.chainId, - stringifyRequiredJson(params.typedData, 'typedData') - ); -} + async getTransactionStatus(txnId: string): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.getTransactionStatus( + this.owner.clientId, + txnId + ); + } -export async function sendTransaction( - params: SendTransactionParams -): Promise { - return withFeeOptionSelector(params.selectFeeOption, (selectorId) => - OmsClientReactNativeSdk.sendTransaction( + async verifyMessageSignature( + params: VerifyMessageSignatureParams + ): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.verifyMessageSignature( + this.owner.clientId, params.chainId, - params.to, - params.value, - params.data ?? null, - params.mode ?? null, - selectorId, - params.waitForStatus ?? true, - stringifyOptionalNumber(params.statusPolling?.timeoutMs), - stringifyOptionalNumber(params.statusPolling?.intervalMs), - stringifyOptionalNumber(params.statusPolling?.fastIntervalMs), - stringifyOptionalNumber(params.statusPolling?.fastPollCount) - ) - ); -} + params.message, + params.signature + ); + } -export async function callContract( - params: CallContractParams -): Promise { - return withFeeOptionSelector(params.selectFeeOption, (selectorId) => - OmsClientReactNativeSdk.callContract( + async verifyTypedDataSignature( + params: VerifyTypedDataSignatureParams + ): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.verifyTypedDataSignature( + this.owner.clientId, params.chainId, - params.contractAddress, - params.method, - stringifyOptionalJson(params.args), - params.mode ?? null, - selectorId, - params.waitForStatus ?? true, - stringifyOptionalNumber(params.statusPolling?.timeoutMs), - stringifyOptionalNumber(params.statusPolling?.intervalMs), - stringifyOptionalNumber(params.statusPolling?.fastIntervalMs), - stringifyOptionalNumber(params.statusPolling?.fastPollCount) - ) - ); -} - -export function getTransactionStatus( - txnId: string -): Promise { - return OmsClientReactNativeSdk.getTransactionStatus(txnId); -} - -export function getTokenBalances( - params: GetTokenBalancesParams -): Promise { - return OmsClientReactNativeSdk.getTokenBalances( - params.chainId, - params.contractAddress ?? null, - params.walletAddress, - params.includeMetadata ?? false, - params.page?.page == null ? null : String(params.page.page), - params.page?.pageSize == null ? null : String(params.page.pageSize) - ); -} + stringifyRequiredJson(params.typedData, 'typedData'), + params.signature + ); + } -export function getNativeTokenBalance( - params: GetNativeTokenBalanceParams -): Promise { - return OmsClientReactNativeSdk.getNativeTokenBalance( - params.chainId, - params.walletAddress - ); -} + async getIdToken(params: GetIdTokenParams = {}): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.getIdToken( + this.owner.clientId, + params.ttlSeconds == null ? null : String(params.ttlSeconds), + stringifyOptionalJson(params.customClaims) + ); + } -export function verifyMessageSignature( - params: VerifyMessageSignatureParams -): Promise { - return OmsClientReactNativeSdk.verifyMessageSignature( - params.chainId, - params.message, - params.signature - ); -} + async listAccess( + params: ListAccessParams = {} + ): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.listAccess( + this.owner.clientId, + params.pageSize == null ? null : String(params.pageSize) + ); + } -export function verifyTypedDataSignature( - params: VerifyTypedDataSignatureParams -): Promise { - return OmsClientReactNativeSdk.verifyTypedDataSignature( - params.chainId, - stringifyRequiredJson(params.typedData, 'typedData'), - params.signature - ); -} + async *listAccessPages( + params: ListAccessPagesParams = {} + ): AsyncGenerator { + let cursor: string | null = null; + + do { + const response = await this.listAccessPage({ + pageSize: params.pageSize, + cursor, + }); + yield response; + cursor = response.page?.cursor ?? null; + } while (cursor != null); + } -export function getIdToken(params: GetIdTokenParams = {}): Promise { - return OmsClientReactNativeSdk.getIdToken( - params.ttlSeconds == null ? null : String(params.ttlSeconds), - stringifyOptionalJson(params.customClaims) - ); -} + async listAccessPage( + params: ListAccessPageParams = {} + ): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.listAccessPage( + this.owner.clientId, + params.pageSize == null ? null : String(params.pageSize), + params.cursor ?? null + ); + } -export function listAccess( - params: ListAccessParams = {} -): Promise { - return OmsClientReactNativeSdk.listAccess( - params.pageSize == null ? null : String(params.pageSize) - ); + async revokeAccess(targetCredentialId: string): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.revokeAccess( + this.owner.clientId, + targetCredentialId + ); + } } -export async function* listAccessPages( - params: ListAccessPagesParams = {} -): AsyncGenerator { - let cursor: string | null = null; - - do { - const response = await listAccessPage({ - pageSize: params.pageSize, - cursor, - }); - yield response; - cursor = response.page?.cursor ?? null; - } while (cursor != null); -} +export class OMSIndexerClient { + constructor(private readonly owner: OMSClient) {} -export function listAccessPage( - params: ListAccessPageParams = {} -): Promise { - return OmsClientReactNativeSdk.listAccessPage( - params.pageSize == null ? null : String(params.pageSize), - params.cursor ?? null - ); -} + async getBalances(params: GetBalancesParams): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.getBalances( + this.owner.clientId, + serializeIndexerParams(params) + ); + } -export function revokeAccess(targetCredentialId: string): Promise { - return OmsClientReactNativeSdk.revokeAccess(targetCredentialId); + async getTransactionHistory( + params: GetTransactionHistoryParams + ): Promise { + await this.owner.ensureReady(); + return OmsClientReactNativeSdk.getTransactionHistory( + this.owner.clientId, + serializeIndexerParams(params) + ); + } } diff --git a/src/client.ts b/src/client.ts index 660896c..84d03ee 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,14 +1,17 @@ +import type { EventSubscription } from 'react-native'; +import { supportedNetworks } from './networks'; import type { CallContractParams, CompleteEmailAuthParams, CreateWalletParams, - HandleOidcRedirectCallbackParams, + GetBalancesParams, GetIdTokenParams, - GetNativeTokenBalanceParams, - GetTokenBalancesParams, + GetTransactionHistoryParams, + HandleOidcRedirectCallbackParams, ListAccessPageParams, ListAccessPagesParams, ListAccessParams, + OmsBalancesResult, OmsClientConfig, OmsClientSessionExpiredEvent, OmsClientSessionState, @@ -19,8 +22,7 @@ import type { OmsOidcRedirectAuthResult, OmsSendTransactionResponse, OmsStartOidcRedirectAuthResult, - OmsTokenBalance, - OmsTokenBalancesResult, + OmsTransactionHistoryResult, OmsTransactionStatus, OmsWallet, OmsWalletActivationResult, @@ -31,7 +33,6 @@ import type { VerifyMessageSignatureParams, VerifyTypedDataSignatureParams, } from './types'; -import type { EventSubscription } from 'react-native'; function unsupported(): never { throw new Error( @@ -39,151 +40,147 @@ function unsupported(): never { ); } -export function configure(_config: OmsClientConfig): Promise { - unsupported(); -} - -export function getWalletAddress(): Promise { - unsupported(); -} - -export function getSession(): Promise { - unsupported(); -} - -export function onSessionExpired( - _listener: (event: OmsClientSessionExpiredEvent) => void -): EventSubscription { - unsupported(); -} - -export function getSupportedNetworks(): Promise { - unsupported(); -} - -export function startEmailAuth(_email: string): Promise { - unsupported(); -} - -export function completeEmailAuth( - _params: CompleteEmailAuthParams -): Promise { - unsupported(); -} - -export function signInWithOidcIdToken( - _params: SignInWithOidcIdTokenParams -): Promise { - unsupported(); -} - -export function startOidcRedirectAuth( - _params: StartOidcRedirectAuthParams -): Promise { - unsupported(); -} - -export function handleOidcRedirectCallback( - _params: HandleOidcRedirectCallbackParams = {} -): Promise { - unsupported(); -} - -export function listWallets(): Promise { - unsupported(); -} - -export function useWallet( - _walletId: string -): Promise { - unsupported(); -} - -export function createWallet( - _params: CreateWalletParams = {} -): Promise { - unsupported(); -} - -export function signOut(): Promise { - unsupported(); -} - -export function signMessage( - _chainId: string, - _message: string -): Promise { - unsupported(); -} - -export function signTypedData(_params: SignTypedDataParams): Promise { - unsupported(); -} - -export function sendTransaction( - _params: SendTransactionParams -): Promise { - unsupported(); -} - -export function callContract( - _params: CallContractParams -): Promise { - unsupported(); -} - -export function getTransactionStatus( - _txnId: string -): Promise { - unsupported(); -} - -export function getTokenBalances( - _params: GetTokenBalancesParams -): Promise { - unsupported(); -} - -export function getNativeTokenBalance( - _params: GetNativeTokenBalanceParams -): Promise { - unsupported(); -} - -export function verifyMessageSignature( - _params: VerifyMessageSignatureParams -): Promise { - unsupported(); -} - -export function verifyTypedDataSignature( - _params: VerifyTypedDataSignatureParams -): Promise { - unsupported(); -} - -export function getIdToken(_params: GetIdTokenParams = {}): Promise { - unsupported(); -} - -export function listAccess( - _params: ListAccessParams = {} -): Promise { - unsupported(); -} - -export async function* listAccessPages( - _params: ListAccessPagesParams = {} -): AsyncGenerator { - unsupported(); -} - -export function listAccessPage( - _params: ListAccessPageParams = {} -): Promise { - unsupported(); -} - -export function revokeAccess(_targetCredentialId: string): Promise { - unsupported(); +export class OMSClient { + public readonly wallet: OMSWalletClient; + public readonly indexer: OMSIndexerClient; + public readonly supportedNetworks: OmsNetwork[] = supportedNetworks; + + constructor(_config: OmsClientConfig) { + this.wallet = new OMSWalletClient(); + this.indexer = new OMSIndexerClient(); + } +} + +export class OMSWalletClient { + getWalletAddress(): Promise { + unsupported(); + } + + getSession(): Promise { + unsupported(); + } + + onSessionExpired( + _listener: (event: OmsClientSessionExpiredEvent) => void + ): EventSubscription { + unsupported(); + } + + startEmailAuth(_email: string): Promise { + unsupported(); + } + + completeEmailAuth( + _params: CompleteEmailAuthParams + ): Promise { + unsupported(); + } + + signInWithOidcIdToken( + _params: SignInWithOidcIdTokenParams + ): Promise { + unsupported(); + } + + startOidcRedirectAuth( + _params: StartOidcRedirectAuthParams + ): Promise { + unsupported(); + } + + handleOidcRedirectCallback( + _params: HandleOidcRedirectCallbackParams = {} + ): Promise { + unsupported(); + } + + listWallets(): Promise { + unsupported(); + } + + useWallet(_walletId: string): Promise { + unsupported(); + } + + createWallet( + _params: CreateWalletParams = {} + ): Promise { + unsupported(); + } + + signOut(): Promise { + unsupported(); + } + + signMessage(_chainId: string, _message: string): Promise { + unsupported(); + } + + signTypedData(_params: SignTypedDataParams): Promise { + unsupported(); + } + + sendTransaction( + _params: SendTransactionParams + ): Promise { + unsupported(); + } + + callContract( + _params: CallContractParams + ): Promise { + unsupported(); + } + + getTransactionStatus(_txnId: string): Promise { + unsupported(); + } + + verifyMessageSignature( + _params: VerifyMessageSignatureParams + ): Promise { + unsupported(); + } + + verifyTypedDataSignature( + _params: VerifyTypedDataSignatureParams + ): Promise { + unsupported(); + } + + getIdToken(_params: GetIdTokenParams = {}): Promise { + unsupported(); + } + + listAccess(_params: ListAccessParams = {}): Promise { + unsupported(); + } + + async *listAccessPages( + _params: ListAccessPagesParams = {} + ): AsyncGenerator { + unsupported(); + } + + listAccessPage( + _params: ListAccessPageParams = {} + ): Promise { + unsupported(); + } + + revokeAccess(_targetCredentialId: string): Promise { + unsupported(); + } +} + +export class OMSIndexerClient { + getBalances(_params: GetBalancesParams): Promise { + unsupported(); + } + + getTransactionHistory( + _params: GetTransactionHistoryParams + ): Promise { + unsupported(); + } } diff --git a/src/index.tsx b/src/index.tsx index 1644cb2..e24898e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,33 +1,4 @@ -export { - callContract, - completeEmailAuth, - configure, - createWallet, - getIdToken, - getNativeTokenBalance, - getSupportedNetworks, - getSession, - getTokenBalances, - getTransactionStatus, - getWalletAddress, - handleOidcRedirectCallback, - listAccess, - listAccessPages, - listAccessPage, - listWallets, - onSessionExpired, - revokeAccess, - sendTransaction, - signInWithOidcIdToken, - signMessage, - signTypedData, - signOut, - startEmailAuth, - startOidcRedirectAuth, - verifyMessageSignature, - verifyTypedDataSignature, - useWallet, -} from './client'; +export { OMSClient } from './client'; export { OidcProviders } from './oidcProviders'; export { formatUnits, parseUnits } from './units'; export type { ParseUnitsOptions, ParseUnitsRoundingMode } from './units'; @@ -36,28 +7,31 @@ export type { CallContractParams, CompleteEmailAuthParams, CreateWalletParams, - HandleOidcRedirectCallbackParams, + GetBalancesParams, GetIdTokenParams, - GetNativeTokenBalanceParams, - GetTokenBalancesParams, + GetTransactionHistoryParams, GoogleOidcProviderParams, + HandleOidcRedirectCallbackParams, ListAccessPageParams, ListAccessPagesParams, ListAccessParams, OmsAccessPage, + OmsBalancesResult, OmsClientConfig, - OmsClientEnvironment, OmsClientSessionExpiredEvent, OmsClientSessionLoginType, OmsClientSessionState, OmsCompleteAuthResult, + OmsContractVerificationStatus, OmsCredentialInfo, OmsFeeOption, OmsFeeOptionSelection, OmsFeeOptionSelector, OmsFeeOptionWithBalance, OmsFeeToken, + OmsIndexerNetworkType, OmsListAccessResponse, + OmsMetadataOptions, OmsNetwork, OmsOidcRedirectAuthResult, OmsPendingWalletSelection, @@ -65,13 +39,16 @@ export type { OmsStartOidcRedirectAuthResult, OmsTokenBalance, OmsTokenBalancesPage, - OmsTokenBalancesResult, + OmsTokenBalancesPageRequest, OmsTokenContractInfo, OmsTokenMetadata, OmsTokenMetadataAsset, + OmsTransaction, + OmsTransactionHistoryResult, OmsTransactionMode, - OmsTransactionStatusPollingOptions, OmsTransactionStatus, + OmsTransactionStatusPollingOptions, + OmsTransactionTransfer, OmsWallet, OmsWalletActivationResult, OmsWalletSelectionBehavior, diff --git a/src/networks.ts b/src/networks.ts new file mode 100644 index 0000000..a11b0c2 --- /dev/null +++ b/src/networks.ts @@ -0,0 +1,116 @@ +import type { OmsNetwork } from './types'; + +export const supportedNetworks: OmsNetwork[] = [ + { + chainId: '1', + name: 'mainnet', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://etherscan.io', + displayName: 'Ethereum', + }, + { + chainId: '11155111', + name: 'sepolia', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://sepolia.etherscan.io', + displayName: 'Sepolia', + }, + { + chainId: '137', + name: 'polygon', + nativeTokenSymbol: 'POL', + explorerUrl: 'https://polygonscan.com', + displayName: 'Polygon', + }, + { + chainId: '80002', + name: 'amoy', + nativeTokenSymbol: 'POL', + explorerUrl: 'https://amoy.polygonscan.com', + displayName: 'Polygon Amoy', + }, + { + chainId: '42161', + name: 'arbitrum', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://arbiscan.io', + displayName: 'Arbitrum', + }, + { + chainId: '421614', + name: 'arbitrum-sepolia', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://sepolia.arbiscan.io', + displayName: 'Arbitrum Sepolia', + }, + { + chainId: '10', + name: 'optimism', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://optimistic.etherscan.io', + displayName: 'Optimism', + }, + { + chainId: '11155420', + name: 'optimism-sepolia', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://sepolia-optimism.etherscan.io', + displayName: 'Optimism Sepolia', + }, + { + chainId: '8453', + name: 'base', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://basescan.org', + displayName: 'Base', + }, + { + chainId: '84532', + name: 'base-sepolia', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://sepolia.basescan.org', + displayName: 'Base Sepolia', + }, + { + chainId: '56', + name: 'bsc', + nativeTokenSymbol: 'BNB', + explorerUrl: 'https://bscscan.com', + displayName: 'BSC', + }, + { + chainId: '97', + name: 'bsc-testnet', + nativeTokenSymbol: 'BNB', + explorerUrl: 'https://testnet.bscscan.com', + displayName: 'BSC Testnet', + }, + { + chainId: '42170', + name: 'arbitrum-nova', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://nova.arbiscan.io', + displayName: 'Arbitrum Nova', + }, + { + chainId: '43114', + name: 'avalanche', + nativeTokenSymbol: 'AVAX', + explorerUrl: 'https://snowtrace.io', + displayName: 'Avalanche', + }, + { + chainId: '43113', + name: 'avalanche-testnet', + nativeTokenSymbol: 'AVAX', + explorerUrl: 'https://testnet.snowtrace.io', + displayName: 'Avalanche Testnet', + }, + { + chainId: '747474', + name: 'katana', + nativeTokenSymbol: 'ETH', + explorerUrl: 'https://explorer.katanarpc.com', + displayName: 'Katana', + }, +]; diff --git a/src/types.ts b/src/types.ts index 56b4f6d..b9115e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,9 @@ import type { OmsCredentialInfo, OmsFeeOptionSelection, OmsFeeOptionWithBalance, + OmsNetwork, + OmsTokenBalance, + OmsTokenBalancesPage, OmsWallet, OmsWalletActivationResult, } from './NativeOmsClientReactNativeSdk'; @@ -19,11 +22,13 @@ export type { OmsStartOidcRedirectAuthResult, OmsTokenBalance, OmsTokenBalancesPage, - OmsTokenBalancesResult, OmsTokenContractInfo, OmsTokenMetadata, OmsTokenMetadataAsset, + OmsTransaction, + OmsTransactionHistoryResult, OmsTransactionStatus, + OmsTransactionTransfer, OmsWallet, OmsWalletActivationResult, } from './NativeOmsClientReactNativeSdk'; @@ -46,16 +51,8 @@ export type OmsClientSessionExpiredEvent = { expiredAt: string; }; -export type OmsClientEnvironment = { - walletApiUrl?: string; - apiRpcUrl?: string; - indexerUrlTemplate?: string; -}; - export type OmsClientConfig = { publishableKey: string; - projectId: string; - environment?: OmsClientEnvironment; }; export type SendTransactionParams = { @@ -210,20 +207,54 @@ export type CallContractParams = { statusPolling?: OmsTransactionStatusPollingOptions; }; -export type GetTokenBalancesParams = { - chainId: string; - contractAddress?: string; +export type OmsIndexerNetworkType = 'MAINNETS' | 'TESTNETS' | 'ALL'; + +export type OmsContractVerificationStatus = 'VERIFIED' | 'UNVERIFIED' | 'ALL'; + +export type OmsMetadataOptions = { + verifiedOnly?: boolean; + unverifiedOnly?: boolean; + includeContracts?: string[]; +}; + +export type OmsTokenBalancesPageRequest = { + page?: number; + pageSize?: number; +}; + +export type OmsBalancesResult = { + status: number; + page?: OmsTokenBalancesPage | null; + nativeBalances: OmsTokenBalance[]; + balances: OmsTokenBalance[]; +}; + +export type GetBalancesParams = { walletAddress: string; + networks?: OmsNetwork[]; + networkType?: OmsIndexerNetworkType; + contractAddresses?: string[]; includeMetadata?: boolean; - page?: { - page?: number; - pageSize?: number; - }; + omitPrices?: boolean | null; + tokenIds?: string[]; + contractStatus?: OmsContractVerificationStatus | null; + page?: OmsTokenBalancesPageRequest; }; -export type GetNativeTokenBalanceParams = { - chainId: string; +export type GetTransactionHistoryParams = { walletAddress: string; + networks?: OmsNetwork[]; + networkType?: OmsIndexerNetworkType; + contractAddresses?: string[]; + transactionHashes?: string[]; + metaTransactionIds?: string[]; + fromBlock?: number | null; + toBlock?: number | null; + tokenId?: string | null; + includeMetadata?: boolean; + omitPrices?: boolean | null; + metadataOptions?: OmsMetadataOptions | null; + page?: OmsTokenBalancesPageRequest; }; export type VerifyMessageSignatureParams = { diff --git a/test/client.native.test.js b/test/client.native.test.js index bdd7053..60b2286 100644 --- a/test/client.native.test.js +++ b/test/client.native.test.js @@ -11,7 +11,6 @@ const nativeModulePath = path.join( const config = { publishableKey: 'test-publishable-key', - projectId: 'test-project', }; function wallet(id = 'wallet-1') { @@ -107,10 +106,33 @@ function loadClient(overrides = {}) { }; } ); - native.configure = makeRecorder( + native.onFeeOptionSelectionRequest = makeRecorder( calls, - 'configure', - overrides.configure ?? (async () => undefined) + 'onFeeOptionSelectionRequest', + (listener) => { + native.feeOptionSelectionListener = listener; + return { + remove() { + native.feeOptionSelectionListener = null; + }, + }; + } + ); + native.createClient = makeRecorder( + calls, + 'createClient', + overrides.createClient ?? (async () => undefined) + ); + native.getSession = makeRecorder( + calls, + 'getSession', + overrides.getSession ?? + (async () => ({ + walletAddress: null, + expiresAt: null, + loginType: null, + sessionEmail: null, + })) ); native.startEmailAuth = makeRecorder( calls, @@ -170,6 +192,28 @@ function loadClient(overrides = {}) { 'signOut', overrides.signOut ?? (async () => undefined) ); + native.getBalances = makeRecorder( + calls, + 'getBalances', + overrides.getBalances ?? + (async () => ({ + status: 200, + page: null, + nativeBalances: [], + balances: [], + })) + ); + native.getTransactionHistory = makeRecorder( + calls, + 'getTransactionHistory', + overrides.getTransactionHistory ?? + (async () => ({ status: 200, page: null, transactions: [] })) + ); + native.respondToFeeOptionSelection = makeRecorder( + calls, + 'respondToFeeOptionSelection', + overrides.respondToFeeOptionSelection ?? (async () => undefined) + ); require.cache[nativeModuleId] = { id: nativeModuleId, @@ -188,18 +232,18 @@ function loadClient(overrides = {}) { }; } -async function configure(client) { - await client.configure(config); +function createOms(client) { + return new client.OMSClient(config); } -function emitSessionExpired(native, event) { +function emitSessionExpired(native, clientId, event) { assert.equal(typeof native.sessionExpiredListener, 'function'); - native.sessionExpiredListener(event); + native.sessionExpiredListener({ clientId, ...event }); } -function subscribe(client) { +function subscribe(oms) { const events = []; - const subscription = client.onSessionExpired((event) => { + const subscription = oms.wallet.onSessionExpired((event) => { events.push(event); }); return { events, subscription }; @@ -207,106 +251,125 @@ function subscribe(client) { async function expectReplayCleared(action) { const { client, native } = loadClient(); + const oms = createOms(client); const staleEvent = sessionExpiredEvent('stale'); - await configure(client); - emitSessionExpired(native, staleEvent); + emitSessionExpired(native, 'oms-client-1', staleEvent); - await action(client, native); + await action(oms, native); - const { events } = subscribe(client); + const { events } = subscribe(oms); assert.deepEqual(events, []); } -test('replays native session expiry to late JS subscribers and fans out future events', async () => { +test('creates a native client and routes instance calls with its client id', async () => { + const { calls, client } = loadClient(); + const oms = createOms(client); + + await oms.wallet.getSession(); + + assert.deepEqual(calls.createClient[0], [ + 'oms-client-1', + 'test-publishable-key', + ]); + assert.deepEqual(calls.getSession[0], ['oms-client-1']); + assert.equal( + oms.supportedNetworks.some((network) => network.chainId === '137'), + true + ); +}); + +test('replays native session expiry only to matching client subscribers', () => { + const { client, native } = loadClient(); + const firstOms = createOms(client); + const secondOms = createOms(client); const firstEvent = sessionExpiredEvent('first'); const secondEvent = sessionExpiredEvent('second'); const thirdEvent = sessionExpiredEvent('third'); - let native; - const loaded = loadClient({ - configure: async () => { - emitSessionExpired(native, firstEvent); - }, - }); - native = loaded.native; - - await configure(loaded.client); - assert.equal(loaded.calls.onSessionExpired.length, 1); + emitSessionExpired(native, 'oms-client-1', firstEvent); - const firstSubscriber = subscribe(loaded.client); + const firstSubscriber = subscribe(firstOms); + const secondSubscriber = subscribe(secondOms); assert.deepEqual(firstSubscriber.events, [firstEvent]); + assert.deepEqual(secondSubscriber.events, []); - const secondSubscriber = subscribe(loaded.client); - assert.deepEqual(secondSubscriber.events, [firstEvent]); - assert.equal(loaded.calls.onSessionExpired.length, 1); - - emitSessionExpired(native, secondEvent); + emitSessionExpired(native, 'oms-client-1', secondEvent); assert.deepEqual(firstSubscriber.events, [firstEvent, secondEvent]); - assert.deepEqual(secondSubscriber.events, [firstEvent, secondEvent]); + assert.deepEqual(secondSubscriber.events, []); - firstSubscriber.subscription.remove(); - emitSessionExpired(native, thirdEvent); + emitSessionExpired(native, 'oms-client-2', thirdEvent); + assert.deepEqual(secondSubscriber.events, [thirdEvent]); + firstSubscriber.subscription.remove(); + emitSessionExpired(native, 'oms-client-1', thirdEvent); assert.deepEqual(firstSubscriber.events, [firstEvent, secondEvent]); - assert.deepEqual(secondSubscriber.events, [ - firstEvent, - secondEvent, - thirdEvent, - ]); }); test('clears cached session expiry when auth or session state is reset', async () => { - await expectReplayCleared((client) => configure(client)); - await expectReplayCleared((client) => - client.startEmailAuth('user@example.com') + await expectReplayCleared((oms) => + oms.wallet.startEmailAuth('user@example.com') ); - await expectReplayCleared((client) => - client.startOidcRedirectAuth({ - provider: { id: 'google' }, + await expectReplayCleared((oms) => + oms.wallet.startOidcRedirectAuth({ + provider: { + issuer: 'issuer', + clientId: 'client', + authorizationUrl: 'url', + }, redirectUri: 'example://auth', }) ); - await expectReplayCleared((client) => - client.signInWithOidcIdToken({ + await expectReplayCleared((oms) => + oms.wallet.signInWithOidcIdToken({ idToken: 'id-token', issuer: 'https://issuer.example.com', audience: 'audience', }) ); - await expectReplayCleared((client) => - client.completeEmailAuth({ code: '123456' }) + await expectReplayCleared((oms) => + oms.wallet.completeEmailAuth({ code: '123456' }) ); - await expectReplayCleared((client) => - client.handleOidcRedirectCallback({ + await expectReplayCleared((oms) => + oms.wallet.handleOidcRedirectCallback({ callbackUrl: 'example://auth?code=abc', }) ); - await expectReplayCleared((client) => client.useWallet('wallet-1')); - await expectReplayCleared((client) => client.createWallet()); - await expectReplayCleared((client) => client.signOut()); + await expectReplayCleared((oms) => oms.wallet.useWallet('wallet-1')); + await expectReplayCleared((oms) => oms.wallet.createWallet()); + await expectReplayCleared((oms) => oms.wallet.signOut()); }); -test('clears cached session expiry when pending wallet selection activates a wallet', async () => { +test('routes pending wallet selection activation with the owning client id', async () => { for (const selectionAction of ['selectWallet', 'createAndSelectWallet']) { - const { client, native } = loadClient({ + const { calls, client, native } = loadClient({ completeEmailAuth: async () => pendingWalletSelectionResult(), }); - await configure(client); + const oms = createOms(client); - const result = await client.completeEmailAuth({ + const result = await oms.wallet.completeEmailAuth({ code: '123456', walletSelection: 'manual', }); const staleEvent = sessionExpiredEvent(selectionAction); - emitSessionExpired(native, staleEvent); + emitSessionExpired(native, 'oms-client-1', staleEvent); if (selectionAction === 'selectWallet') { await result.pendingSelection.selectWallet('wallet-1'); + assert.deepEqual(calls.selectWalletForPendingSelection[0], [ + 'oms-client-1', + 'pending-1', + 'wallet-1', + ]); } else { await result.pendingSelection.createAndSelectWallet('reference'); + assert.deepEqual(calls.createAndSelectWalletForPendingSelection[0], [ + 'oms-client-1', + 'pending-1', + 'reference', + ]); } - const { events } = subscribe(client); + const { events } = subscribe(oms); assert.deepEqual(events, []); } }); @@ -316,37 +379,45 @@ test('does not clear cached session expiry for ignored OIDC redirect callbacks', const { client, native } = loadClient({ handleOidcRedirectCallback: async () => ({ type }), }); + const oms = createOms(client); const staleEvent = sessionExpiredEvent(type); - await configure(client); - emitSessionExpired(native, staleEvent); + emitSessionExpired(native, 'oms-client-1', staleEvent); - assert.deepEqual(await client.handleOidcRedirectCallback(), { type }); + assert.deepEqual(await oms.wallet.handleOidcRedirectCallback(), { type }); - const { events } = subscribe(client); + const { events } = subscribe(oms); assert.deepEqual(events, [staleEvent]); } }); test('passes auth session lifetime and login hint parameters to native', async () => { const { calls, client } = loadClient(); + const oms = createOms(client); - await client.completeEmailAuth({ + await oms.wallet.completeEmailAuth({ code: '123456', walletSelection: 'manual', walletType: 'ethereum', sessionLifetimeSeconds: 3600, }); - await client.completeEmailAuth({ code: '654321' }); + await oms.wallet.completeEmailAuth({ code: '654321' }); assert.deepEqual(calls.completeEmailAuth[0], [ + 'oms-client-1', '123456', 'manual', 'ethereum', '3600', ]); - assert.deepEqual(calls.completeEmailAuth[1], ['654321', null, null, null]); + assert.deepEqual(calls.completeEmailAuth[1], [ + 'oms-client-1', + '654321', + null, + null, + null, + ]); - await client.signInWithOidcIdToken({ + await oms.wallet.signInWithOidcIdToken({ idToken: 'id-token', issuer: 'https://issuer.example.com', audience: 'audience', @@ -355,6 +426,7 @@ test('passes auth session lifetime and login hint parameters to native', async ( sessionLifetimeSeconds: 7200, }); assert.deepEqual(calls.signInWithOidcIdToken[0], [ + 'oms-client-1', 'id-token', 'https://issuer.example.com', 'audience', @@ -363,37 +435,46 @@ test('passes auth session lifetime and login hint parameters to native', async ( '7200', ]); - await client.handleOidcRedirectCallback({ + await oms.wallet.handleOidcRedirectCallback({ callbackUrl: 'example://auth?code=abc', walletSelection: 'manual', sessionLifetimeSeconds: 1800, }); - await client.handleOidcRedirectCallback(); + await oms.wallet.handleOidcRedirectCallback(); assert.deepEqual(calls.handleOidcRedirectCallback[0], [ + 'oms-client-1', 'example://auth?code=abc', 'manual', '1800', ]); - assert.deepEqual(calls.handleOidcRedirectCallback[1], [null, null, null]); + assert.deepEqual(calls.handleOidcRedirectCallback[1], [ + 'oms-client-1', + null, + null, + null, + ]); const provider = { - id: 'google', + issuer: 'issuer', + clientId: 'client', + authorizationUrl: 'https://auth.example.com', relayRedirectUri: 'https://relay.example.com/callback', }; - await client.startOidcRedirectAuth({ + await oms.wallet.startOidcRedirectAuth({ provider, redirectUri: 'example://auth', walletType: 'ethereum', authorizeParams: { prompt: 'select_account' }, loginHint: 'user@example.com', }); - await client.startOidcRedirectAuth({ + await oms.wallet.startOidcRedirectAuth({ provider, redirectUri: 'example://auth', relayRedirectUri: null, }); assert.deepEqual(calls.startOidcRedirectAuth[0], [ + 'oms-client-1', JSON.stringify(provider), 'example://auth', 'ethereum', @@ -402,6 +483,7 @@ test('passes auth session lifetime and login hint parameters to native', async ( 'user@example.com', ]); assert.deepEqual(calls.startOidcRedirectAuth[1], [ + 'oms-client-1', JSON.stringify(provider), 'example://auth', null, @@ -410,3 +492,39 @@ test('passes auth session lifetime and login hint parameters to native', async ( null, ]); }); + +test('serializes indexer balance and transaction history params for native', async () => { + const { calls, client } = loadClient(); + const oms = createOms(client); + const polygon = oms.supportedNetworks.find( + (network) => network.chainId === '137' + ); + + await oms.indexer.getBalances({ + walletAddress: '0xwallet', + networks: [polygon], + includeMetadata: false, + page: { page: 1, pageSize: 25 }, + }); + assert.equal(calls.getBalances[0][0], 'oms-client-1'); + assert.deepEqual(JSON.parse(calls.getBalances[0][1]), { + walletAddress: '0xwallet', + networks: ['137'], + includeMetadata: false, + page: { page: 1, pageSize: 25 }, + }); + + await oms.indexer.getTransactionHistory({ + walletAddress: '0xwallet', + networks: [polygon], + transactionHashes: ['0xtxn'], + metadataOptions: { includeContracts: ['0xcontract'] }, + }); + assert.equal(calls.getTransactionHistory[0][0], 'oms-client-1'); + assert.deepEqual(JSON.parse(calls.getTransactionHistory[0][1]), { + walletAddress: '0xwallet', + networks: ['137'], + transactionHashes: ['0xtxn'], + metadataOptions: { includeContracts: ['0xcontract'] }, + }); +}); From f76d5f2b294d3bea675920b18011b9f7e7697b24 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 25 Jun 2026 21:57:27 +0300 Subject: [PATCH 2/8] docs: tighten SDK usage documentation --- API.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++----- PUBLISHING.md | 6 ++++- README.md | 10 +++---- 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/API.md b/API.md index c9b4872..003a2d4 100644 --- a/API.md +++ b/API.md @@ -18,16 +18,21 @@ iOS resolves `oms-client-swift-sdk` `0.1.0-alpha.3`. import { OMSClient } from '@0xsequence/oms-react-native-sdk'; const oms = new OMSClient({ - publishableKey: string; + publishableKey: '', }); ``` ```ts -type OMSClient = { +type OmsClientConfig = { + publishableKey: string; +}; + +class OMSClient { + constructor(config: OmsClientConfig); wallet: OMSWalletClient; indexer: OMSIndexerClient; supportedNetworks: OmsNetwork[]; -}; +} ``` ## Wallet @@ -113,11 +118,13 @@ oms.wallet.startOidcRedirectAuth({ relayRedirectUri?: string | null; authorizeParams?: Record | null; loginHint?: string | null; -}): Promise<{ +}): Promise + +type OmsStartOidcRedirectAuthResult = { authorizationUrl: string; state: string; challenge: string; -}> +}; oms.wallet.handleOidcRedirectCallback({ callbackUrl?: string | null; @@ -133,6 +140,12 @@ system auth browser, then pass the resulting app-link URL to ### Auth Results ```ts +type OmsCredentialInfo = { + credentialId: string; + expiresAt: string; + isCaller: boolean; +}; + type OmsCompleteAuthResult = | { type: 'walletSelected'; @@ -281,6 +294,23 @@ type OmsFeeOptionSelection = { token: string; }; +type OmsFeeToken = { + network: string; + name: string; + symbol: string; + type: string; + decimals: number | null; + logoUrl: string | null; + contractAddress: string | null; + tokenId: string | null; +}; + +type OmsFeeOption = { + token: OmsFeeToken; + value: string; + displayValue: string; +}; + type OmsFeeOptionWithBalance = { feeOption: OmsFeeOption; selection: OmsFeeOptionSelection; @@ -315,6 +345,18 @@ oms.wallet.listAccessPage({ oms.wallet.revokeAccess(targetCredentialId: string): Promise ``` +```ts +type OmsAccessPage = { + limit: number | null; + cursor: string | null; +}; + +type OmsListAccessResponse = { + credentials: OmsCredentialInfo[]; + page: OmsAccessPage | null; +}; +``` + ## Indexer All indexer APIs are accessed through `oms.indexer`. @@ -382,6 +424,21 @@ type OmsTransaction = { transfers?: OmsTransactionTransfer[] | null; timestamp?: string | null; }; + +type OmsTransactionTransfer = { + transferType?: string | null; + contractAddress?: string | null; + contractType?: string | null; + from?: string | null; + to?: string | null; + tokenIds?: string[] | null; + amounts?: string[] | null; + logIndex?: number | null; + amountsUSD?: string[] | null; + pricesUSD?: string[] | null; + contractInfo?: OmsTokenContractInfo | null; + tokenMetadata?: unknown | null; +}; ``` Pass `networks` for explicit chain selection. If omitted, the native SDK uses @@ -442,7 +499,13 @@ the native SDK token metadata objects. ```ts parseUnits(value: string, decimals?: number, options?: ParseUnitsOptions): string -formatUnits(value: string, decimals?: number): string +formatUnits(value: string | bigint, decimals?: number): string + +type ParseUnitsRoundingMode = 'reject' | 'nearest'; + +type ParseUnitsOptions = { + roundingMode?: ParseUnitsRoundingMode; +}; ``` By default `parseUnits` rounds fractional precision beyond `decimals` to the diff --git a/PUBLISHING.md b/PUBLISHING.md index fa1384f..3a121bc 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -6,7 +6,7 @@ Only maintainers with npm publish access should publish. Publish from `master` a ## 1. Choose The Version -Pre-release versions use `0.x.y-alpha.N`, for example `0.1.0-alpha.2`. +Pre-release versions use `0.x.y-alpha.N`, for example `0.1.0-alpha.3`. Check that the version is not already published: @@ -83,5 +83,9 @@ Verify npm sees the published version: npm view @0xsequence/oms-react-native-sdk@ version ``` +If the release updates APIs used by the standalone Expo example, update +`examples/expo-example` to depend on the newly published npm version after npm +confirms it is available. + If the package should become the default install later, move the npm dist-tag deliberately in a separate step. diff --git a/README.md b/README.md index 141bf7a..d954eef 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,7 @@ npm install @0xsequence/oms-react-native-sdk ## Usage ```ts -import { - OMSClient, - OidcProviders, - formatUnits, - parseUnits, -} from '@0xsequence/oms-react-native-sdk'; +import { OMSClient } from '@0xsequence/oms-react-native-sdk'; const oms = new OMSClient({ publishableKey: '', @@ -47,6 +42,7 @@ provider OAuth in an embedded WebView. ```ts import { InAppBrowser } from 'react-native-inappbrowser-reborn'; +import { OidcProviders } from '@0xsequence/oms-react-native-sdk'; const started = await oms.wallet.startOidcRedirectAuth({ provider: OidcProviders.google(), @@ -116,6 +112,8 @@ const txResult = await oms.wallet.sendTransaction({ ### Unit Formatting ```ts +import { formatUnits, parseUnits } from '@0xsequence/oms-react-native-sdk'; + const raw = parseUnits('12.34', 6); // "12340000" const formatted = formatUnits(raw, 6); // "12.34" const rounded = parseUnits('1.235', 2); // "124" From bb70fbd3f1df88f1c7d8c575c2f59e403df93bc2 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 25 Jun 2026 22:00:10 +0300 Subject: [PATCH 3/8] docs: focus API reference on public surface --- API.md | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/API.md b/API.md index 003a2d4..8c80f6d 100644 --- a/API.md +++ b/API.md @@ -9,9 +9,6 @@ This document describes the public TypeScript API for npm install @0xsequence/oms-react-native-sdk ``` -Android resolves `io.github.0xsequence:oms-client-kotlin-sdk:0.1.0-alpha.3`. -iOS resolves `oms-client-swift-sdk` `0.1.0-alpha.3`. - ## Client ```ts @@ -23,18 +20,15 @@ const oms = new OMSClient({ ``` ```ts +new OMSClient(config: OmsClientConfig) + type OmsClientConfig = { publishableKey: string; }; - -class OMSClient { - constructor(config: OmsClientConfig); - wallet: OMSWalletClient; - indexer: OMSIndexerClient; - supportedNetworks: OmsNetwork[]; -} ``` +`OMSClient` exposes `wallet`, `indexer`, and `supportedNetworks`. + ## Wallet All wallet APIs are accessed through `oms.wallet`. @@ -46,7 +40,7 @@ oms.wallet.getWalletAddress(): Promise oms.wallet.getSession(): Promise oms.wallet.onSessionExpired( listener: (event: OmsClientSessionExpiredEvent) => void -): EventSubscription +): { remove(): void } oms.wallet.signOut(): Promise ``` @@ -441,8 +435,8 @@ type OmsTransactionTransfer = { }; ``` -Pass `networks` for explicit chain selection. If omitted, the native SDK uses -`networkType`, which defaults to `MAINNETS`. +Pass `networks` for explicit chain selection. If omitted, `networkType` +defaults to `MAINNETS`. ## Networks @@ -492,8 +486,8 @@ type OmsTokenBalancesPage = { }; ``` -`OmsTokenContractInfo`, `OmsTokenMetadata`, and `OmsTokenMetadataAsset` mirror -the native SDK token metadata objects. +The SDK also exports `OmsTokenContractInfo`, `OmsTokenMetadata`, and +`OmsTokenMetadataAsset` for metadata returned in these fields. ## Formatting Helpers From e8eb73d0cb8b6639af89b67d129bfa890c5b2958 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 25 Jun 2026 22:02:54 +0300 Subject: [PATCH 4/8] ci: align Claude review workflow --- .github/workflows/claude-code-review.yml | 34 ------------ .github/workflows/claude-review.yml | 66 ++++++++++++++++++++++++ .github/workflows/claude.yml | 37 ------------- 3 files changed, 66 insertions(+), 71 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml create mode 100644 .github/workflows/claude-review.yml delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index 21f554e..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize] - -permissions: - contents: read - pull-requests: write - id-token: write - -jobs: - review: - if: github.event.pull_request.user.login != 'dependabot[bot]' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Claude Code Review - uses: anthropics/claude-code-action@beta - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_GITHUB_ACTIONS }} - github_token: ${{ secrets.GITHUB_TOKEN }} - direct_prompt: | - Review this pull request. Focus on: - - Correctness of the TypeScript API surface changes - - Whether API.md is updated if src/index.tsx exports changed - - Native layer consistency (android/ and ios/ should change together) - - Conventional Commits format on the PR title - - Any obvious bugs, type errors, or missing edge cases - use_sticky_comment: true diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml new file mode 100644 index 0000000..e23f4ad --- /dev/null +++ b/.github/workflows/claude-review.yml @@ -0,0 +1,66 @@ +name: Claude Review + +on: + pull_request: + types: [opened, ready_for_review] + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +jobs: + initial-review: + if: >- + ${{ + github.event_name == 'pull_request' && + !github.event.pull_request.draft && + github.event.pull_request.user.login != 'dependabot[bot]' + }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + id-token: write + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Run Claude Code Review + uses: anthropics/claude-code-action@beta + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_GITHUB_ACTIONS }} + github_token: ${{ github.token }} + model: 'claude-opus-4-8' + direct_prompt: | + Review this pull request. Focus on: + - Correctness of the TypeScript public API and exported types + - Native bridge consistency across src/, android/, and ios/ + - Auth, session, wallet selection, signing, and transaction security + - No backwards compatibility shims unless explicitly approved + - API.md, README.md, and PUBLISHING.md updates for public API or release changes + - Expo example dependency policy: update only after the npm version is published + - Test coverage and verification for changed behavior + Be concise. Flag blockers clearly; call out nits as nits. + + requested-review: + if: >- + (github.event_name == 'pull_request_review_comment' || github.event.issue.pull_request != null) && + contains(github.event.comment.body, '@claude review') + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + id-token: write + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Run Claude Code Review + uses: anthropics/claude-code-action@beta + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_GITHUB_ACTIONS }} + github_token: ${{ github.token }} + model: 'claude-opus-4-8' + trigger_phrase: '@claude review' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 21fe37a..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Claude - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened] - pull_request_review: - types: [submitted] - -permissions: - contents: write - pull-requests: write - issues: write - id-token: write - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 1 - - - name: Run Claude - uses: anthropics/claude-code-action@beta - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_GITHUB_ACTIONS }} - github_token: ${{ secrets.GITHUB_TOKEN }} From ff64861cfe89644d15cadaa315e6d8520edf3c33 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 25 Jun 2026 22:27:42 +0300 Subject: [PATCH 5/8] ci: pin Ruby for iOS CocoaPods --- .github/workflows/ci.yml | 6 ++++++ .ruby-version | 1 + examples/sdk-example/Gemfile | 1 + examples/sdk-example/Gemfile.lock | 6 ++++-- examples/trails-actions-example/Gemfile | 1 + examples/trails-actions-example/Gemfile.lock | 6 ++++-- 6 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 .ruby-version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e86353d..b02f2ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,6 +150,12 @@ jobs: with: xcode-version: ${{ env.XCODE_VERSION }} + - name: Setup Ruby + if: env.turbo_cache_hit != '1' + uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1 + with: + ruby-version: '3.4.9' + - name: Install cocoapods if: env.turbo_cache_hit != '1' run: | diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..7bcbb38 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.9 diff --git a/examples/sdk-example/Gemfile b/examples/sdk-example/Gemfile index 584f577..7cc2352 100644 --- a/examples/sdk-example/Gemfile +++ b/examples/sdk-example/Gemfile @@ -15,3 +15,4 @@ gem 'logger' gem 'benchmark' gem 'mutex_m' gem 'nkf' +gem 'base64' diff --git a/examples/sdk-example/Gemfile.lock b/examples/sdk-example/Gemfile.lock index 8196e85..f1c6149 100644 --- a/examples/sdk-example/Gemfile.lock +++ b/examples/sdk-example/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.9) + CFPropertyList (3.0.8) activesupport (6.1.7.10) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -14,6 +14,7 @@ GEM httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) + base64 (0.3.0) benchmark (0.5.0) bigdecimal (4.1.2) claide (1.1.0) @@ -98,6 +99,7 @@ PLATFORMS DEPENDENCIES activesupport (>= 6.1.7.5, != 7.1.0) + base64 benchmark bigdecimal cocoapods (>= 1.13, != 1.15.1, != 1.15.0) @@ -108,7 +110,7 @@ DEPENDENCIES xcodeproj (>= 1.27.0, < 2.0) RUBY VERSION - ruby 2.6.10p210 + ruby 3.4.9p82 BUNDLED WITH 2.4.22 diff --git a/examples/trails-actions-example/Gemfile b/examples/trails-actions-example/Gemfile index 584f577..7cc2352 100644 --- a/examples/trails-actions-example/Gemfile +++ b/examples/trails-actions-example/Gemfile @@ -15,3 +15,4 @@ gem 'logger' gem 'benchmark' gem 'mutex_m' gem 'nkf' +gem 'base64' diff --git a/examples/trails-actions-example/Gemfile.lock b/examples/trails-actions-example/Gemfile.lock index 8196e85..f1c6149 100644 --- a/examples/trails-actions-example/Gemfile.lock +++ b/examples/trails-actions-example/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.9) + CFPropertyList (3.0.8) activesupport (6.1.7.10) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -14,6 +14,7 @@ GEM httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) + base64 (0.3.0) benchmark (0.5.0) bigdecimal (4.1.2) claide (1.1.0) @@ -98,6 +99,7 @@ PLATFORMS DEPENDENCIES activesupport (>= 6.1.7.5, != 7.1.0) + base64 benchmark bigdecimal cocoapods (>= 1.13, != 1.15.1, != 1.15.0) @@ -108,7 +110,7 @@ DEPENDENCIES xcodeproj (>= 1.27.0, < 2.0) RUBY VERSION - ruby 2.6.10p210 + ruby 3.4.9p82 BUNDLED WITH 2.4.22 From 216a75c156dfebc08c53cc8f66ae457ee86f23c2 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 25 Jun 2026 22:33:14 +0300 Subject: [PATCH 6/8] ci: preinstall Android NDK with retry --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b02f2ee..f1fcb33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,7 @@ jobs: runs-on: ubuntu-latest env: + ANDROID_NDK_VERSION: 27.1.12297006 TURBO_CACHE_DIR: .turbo/android steps: @@ -93,6 +94,16 @@ jobs: if: env.turbo_cache_hit != '1' run: | /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" + for attempt in 1 2 3; do + if "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;$ANDROID_NDK_VERSION"; then + exit 0 + fi + + rm -rf "$ANDROID_HOME/ndk/$ANDROID_NDK_VERSION" "$ANDROID_HOME/.temp" + sleep 10 + done + + exit 1 - name: Cache Gradle From 0e0f59e4de4b662a56ee4bdc48af425bf422224f Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Fri, 26 Jun 2026 11:37:02 +0300 Subject: [PATCH 7/8] fix: align fee selection and network metadata --- .../OmsClientReactNativeSdkModule.kt | 7 +- src/networks.ts | 6 +- test/client.native.test.js | 113 ++++++++++++++++++ 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/omsclientreactnativesdk/OmsClientReactNativeSdkModule.kt b/android/src/main/java/com/omsclientreactnativesdk/OmsClientReactNativeSdkModule.kt index 1ce8603..3c21572 100644 --- a/android/src/main/java/com/omsclientreactnativesdk/OmsClientReactNativeSdkModule.kt +++ b/android/src/main/java/com/omsclientreactnativesdk/OmsClientReactNativeSdkModule.kt @@ -904,17 +904,16 @@ class OmsClientReactNativeSdkModule(reactContext: ReactApplicationContext) : private fun feeOptionWithBalanceMap(option: FeeOptionWithBalance): WritableMap = Arguments.createMap().apply { putMap("feeOption", feeOptionMap(option.feeOption)) - putMap("selection", feeOptionSelectionMap(option.feeOption)) + putMap("selection", feeOptionSelectionMap(option.selection)) option.balance?.let { putMap("balance", tokenBalanceMap(it)) } ?: putNull("balance") putNullableString("available", option.available) putNullableString("availableRaw", option.availableRaw) option.decimals?.let { putDouble("decimals", it.toDouble()) } ?: putNull("decimals") } - private fun feeOptionSelectionMap(option: FeeOption): WritableMap = + private fun feeOptionSelectionMap(selection: FeeOptionSelection): WritableMap = Arguments.createMap().apply { - val tokenId = option.token.tokenId?.trim()?.takeIf { it.isNotEmpty() } - putString("token", tokenId ?: option.token.symbol) + putString("token", selection.token) } private fun feeOptionMap(option: FeeOption): WritableMap = diff --git a/src/networks.ts b/src/networks.ts index a11b0c2..cd808fa 100644 --- a/src/networks.ts +++ b/src/networks.ts @@ -96,21 +96,21 @@ export const supportedNetworks: OmsNetwork[] = [ chainId: '43114', name: 'avalanche', nativeTokenSymbol: 'AVAX', - explorerUrl: 'https://snowtrace.io', + explorerUrl: 'https://subnets.avax.network/c-chain', displayName: 'Avalanche', }, { chainId: '43113', name: 'avalanche-testnet', nativeTokenSymbol: 'AVAX', - explorerUrl: 'https://testnet.snowtrace.io', + explorerUrl: 'https://subnets-test.avax.network/c-chain', displayName: 'Avalanche Testnet', }, { chainId: '747474', name: 'katana', nativeTokenSymbol: 'ETH', - explorerUrl: 'https://explorer.katanarpc.com', + explorerUrl: 'https://katanascan.com', displayName: 'Katana', }, ]; diff --git a/test/client.native.test.js b/test/client.native.test.js index 60b2286..ff97b2d 100644 --- a/test/client.native.test.js +++ b/test/client.native.test.js @@ -192,6 +192,16 @@ function loadClient(overrides = {}) { 'signOut', overrides.signOut ?? (async () => undefined) ); + native.sendTransaction = makeRecorder( + calls, + 'sendTransaction', + overrides.sendTransaction ?? + (async () => ({ + txnId: 'txn-1', + status: 'sent', + txnHash: '0xtxn', + })) + ); native.getBalances = makeRecorder( calls, 'getBalances', @@ -278,6 +288,27 @@ test('creates a native client and routes instance calls with its client id', asy ); }); +test('exposes supported network metadata aligned with native SDKs', () => { + const { client } = loadClient(); + const oms = createOms(client); + + assert.equal( + oms.supportedNetworks.find((network) => network.chainId === '43114') + ?.explorerUrl, + 'https://subnets.avax.network/c-chain' + ); + assert.equal( + oms.supportedNetworks.find((network) => network.chainId === '43113') + ?.explorerUrl, + 'https://subnets-test.avax.network/c-chain' + ); + assert.equal( + oms.supportedNetworks.find((network) => network.chainId === '747474') + ?.explorerUrl, + 'https://katanascan.com' + ); +}); + test('replays native session expiry only to matching client subscribers', () => { const { client, native } = loadClient(); const firstOms = createOms(client); @@ -528,3 +559,85 @@ test('serializes indexer balance and transaction history params for native', asy metadataOptions: { includeContracts: ['0xcontract'] }, }); }); + +test('round-trips fee option selection token from native request', async () => { + let capturedFeeOptions; + const feeOption = { + feeOption: { + token: { + network: '137', + name: 'Polygon', + symbol: 'POL', + type: 'native', + decimals: 18, + logoUrl: null, + contractAddress: null, + tokenId: 'fee-token-id', + }, + value: '100', + displayValue: '0.0000000000000001', + }, + selection: { token: 'canonical-selection-token' }, + balance: null, + available: '1', + availableRaw: '1000000000000000000', + decimals: 18, + }; + const { calls, client, native } = loadClient({ + sendTransaction: async ( + _clientId, + _chainId, + _to, + _value, + _data, + _mode, + feeOptionSelectorId + ) => { + assert.equal(feeOptionSelectorId, 'fee-option-selector-1'); + assert.equal(typeof native.feeOptionSelectionListener, 'function'); + await native.feeOptionSelectionListener({ + selectorId: feeOptionSelectorId, + requestId: 'fee-request-1', + options: [feeOption], + }); + return { + txnId: 'txn-1', + status: 'sent', + txnHash: '0xtxn', + }; + }, + }); + const oms = createOms(client); + + const result = await oms.wallet.sendTransaction({ + chainId: '137', + to: '0xrecipient', + value: '0', + selectFeeOption: async (feeOptions) => { + capturedFeeOptions = feeOptions; + return feeOptions[0].selection; + }, + }); + + assert.deepEqual(result, { + txnId: 'txn-1', + status: 'sent', + txnHash: '0xtxn', + }); + assert.deepEqual(capturedFeeOptions, [feeOption]); + assert.deepEqual(calls.respondToFeeOptionSelection[0], [ + 'fee-request-1', + 'canonical-selection-token', + null, + ]); + assert.deepEqual(calls.sendTransaction[0].slice(0, 8), [ + 'oms-client-1', + '137', + '0xrecipient', + '0', + null, + null, + 'fee-option-selector-1', + true, + ]); +}); From 228dca797a7e70e37115d11265ad815eab91e286 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Fri, 26 Jun 2026 12:00:05 +0300 Subject: [PATCH 8/8] chore: prepare 0.1.0-alpha.3 release --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bee25ca..416cbc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [0.1.0-alpha.3] — 2026-06-26 + +### Added +- Support for Android and iOS OMS SDK `0.1.0-alpha.3`. +- `OMSClient` class API with `wallet`, `indexer`, and synchronous `supportedNetworks`. +- Public wallet APIs for OIDC ID-token sign-in, redirect auth, wallet selection, session state, + signing, transaction submission, transaction status, ID tokens, access listing, and access revoke. +- Public indexer APIs for balances and transaction history. +- Public unit parsing and formatting helpers. + +### Changed +- Updated transaction fee selection to use the native SDK fee option selection payload. +- Updated README and API reference for the current public TypeScript API. +- Updated SDK and Trails examples for the alpha.3 public API. + +### Fixed +- Corrected supported network metadata URLs. +- Fixed Android fee option handling and transaction parameter bridging. +- Aligned CI dependency setup for iOS, Android, and Claude review workflows. + ## [0.1.0-alpha.2] — 2026-06-10 ### Added