Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,9 @@ All three variants share the following optional base fields:

When fee options are returned, `selectFeeOption` receives `FeeOptionWithBalance[]`.
Each entry includes the generated `FeeOption` plus the selected wallet's balance
for that fee token when the indexer can load it.
for that fee token when the indexer can load it. Use
`FeeOptionSelector.firstAvailable` to choose the first option the wallet can pay,
or return `option.selection` from a custom selector.

---

Expand Down Expand Up @@ -1138,16 +1140,25 @@ type FeeOptionSelector = (
feeOptions: FeeOptionWithBalance[]
) => FeeOptionSelection | undefined | Promise<FeeOptionSelection | undefined>

const FeeOptionSelector: {
firstAvailable: FeeOptionSelector
}

type FeeOptionWithBalance = {
feeOption: FeeOption
selection: FeeOptionSelection
balance?: TokenBalance
available?: string
availableRaw?: string
decimals?: number
}
```

When no selector is provided, the SDK uses the first required fee option, or no fee option for sponsored transactions.
When no selector is provided, the SDK uses the first required fee option, or no
fee option for sponsored transactions. `FeeOptionSelector.firstAvailable` uses
enriched balances to skip underfunded fee options and selects the first option
the wallet can pay. For custom selectors, return `option.selection` to select
that fee option.

---

Expand Down
20 changes: 6 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ pnpm dev:trails-actions-example
## Quick Start

```typescript
import { Networks, OMSClient, WalletType } from '@0xsequence/typescript-sdk'
import { FeeOptionSelector, Networks, OMSClient, WalletType } from '@0xsequence/typescript-sdk'
import { parseUnits } from 'viem'

const oms = new OMSClient({
Expand All @@ -137,17 +137,8 @@ const tx = await oms.wallet.sendTransaction({
network: Networks.polygon,
to: '0xRecipient',
value: parseUnits('1', 18), // 1 POL
selectFeeOption: (feeOptions) => {
// If this Polygon mainnet transaction is not sponsored, choose a fee token the wallet can pay.
const selectedFeeOption = feeOptions.find(({ feeOption, availableRaw }) =>
availableRaw !== undefined && BigInt(availableRaw) >= BigInt(feeOption.value)
)
if (!selectedFeeOption) {
throw new Error('No fee option has enough balance')
}

return { token: selectedFeeOption.feeOption.token.symbol }
},
// If this Polygon mainnet transaction is not sponsored, choose the first fee token the wallet can pay.
selectFeeOption: FeeOptionSelector.firstAvailable,
})
console.log(tx.txnHash ?? tx.txnId)
```
Expand Down Expand Up @@ -390,7 +381,8 @@ await oms.wallet.sendTransaction({

If WaaS returns fee options, pass a selector to choose one. The selector receives
fee options enriched with the current wallet balance for each token when
available.
available. Use `FeeOptionSelector.firstAvailable` to choose the first option the
wallet can pay, or return `option.selection` from a custom selector.

```typescript
const tx = await oms.wallet.sendTransaction({
Expand All @@ -399,7 +391,7 @@ const tx = await oms.wallet.sendTransaction({
data: '0xa9059cbb000000000000000000000000...',
selectFeeOption: async (feeOptions) => {
const selected = feeOptions.find(option => option.feeOption.token.symbol === 'USDC')
return selected ? { token: selected.feeOption.token.symbol } : undefined
return selected?.selection
},
})
```
Expand Down
2 changes: 1 addition & 1 deletion examples/react/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ function App() {
return
}

feeSelection.current?.resolve({ token: option.feeOption.token.symbol })
feeSelection.current?.resolve(option.selection)
feeSelection.current = null
setFeeOptions([])
setWalletStatus(`Selected ${option.feeOption.token.symbol}. Sending transaction...`)
Expand Down
2 changes: 1 addition & 1 deletion examples/trails-actions/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,7 @@ function App() {
}

selectedFeeOption.current = option
feeSelection.current?.resolve({ token: option.feeOption.token.symbol })
feeSelection.current?.resolve(option.selection)
feeSelection.current = null
setFeeOptions([])
appendLog(`Selected ${option.feeOption.token.symbol}.`)
Expand Down
2 changes: 1 addition & 1 deletion examples/wagmi/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export function App() {
return
}

feeOptionSelection.resolveFeeOption({ token: option.feeOption.token.symbol })
feeOptionSelection.resolveFeeOption(option.selection)
setWalletStatus(`Selected ${option.feeOption.token.symbol}. Sending transaction...`)
}

Expand Down
18 changes: 4 additions & 14 deletions examples/wagmi/src/feeOptionSelectionBridge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FeeOptionSelection, FeeOptionWithBalance } from '@0xsequence/typescript-sdk'
import { FeeOptionSelector, type FeeOptionSelection, type FeeOptionWithBalance } from '@0xsequence/typescript-sdk'

export type FeeOptionSelectionRequest = {
options: FeeOptionWithBalance[]
Expand All @@ -25,11 +25,11 @@ export function subscribeToFeeOptionSelection(nextListener: FeeOptionSelectionLi

export async function selectFeeOptionWithAppUi(options: FeeOptionWithBalance[]): Promise<FeeOptionSelection | undefined> {
if (!listener) {
const payableOption = options.find(canPayFeeOption)
if (!payableOption) {
const selection = FeeOptionSelector.firstAvailable(options)
if (!selection) {
throw new Error('No fee option has enough balance.')
}
return { token: payableOption.feeOption.token.symbol }
return selection
}

rejectPendingSelection?.(new Error('Fee option selection was superseded.'))
Expand All @@ -49,13 +49,3 @@ export async function selectFeeOptionWithAppUi(options: FeeOptionWithBalance[]):
})
})
}

function canPayFeeOption(option: FeeOptionWithBalance): boolean {
if (option.availableRaw === undefined) return false

try {
return BigInt(option.availableRaw) >= BigInt(option.feeOption.value)
} catch {
return false
}
}
19 changes: 4 additions & 15 deletions packages/oms-wallet-wagmi-connector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,14 @@ Wagmi connector for an active `@0xsequence/typescript-sdk` OMS client.
```ts
import { createConfig, http } from 'wagmi'
import { polygon } from 'wagmi/chains'
import { OMSClient, type FeeOptionWithBalance } from '@0xsequence/typescript-sdk'
import { FeeOptionSelector, OMSClient } from '@0xsequence/typescript-sdk'
import { omsWalletConnector } from '@0xsequence/oms-wallet-wagmi-connector'

const oms = new OMSClient({
publishableKey: import.meta.env.VITE_OMS_PUBLISHABLE_KEY,
projectId: import.meta.env.VITE_OMS_PROJECT_ID,
})

function selectFirstPayableFeeOption(feeOptions: FeeOptionWithBalance[]) {
const payableOption = feeOptions.find((option) =>
option.availableRaw !== undefined &&
BigInt(option.availableRaw) >= BigInt(option.feeOption.value)
)
if (!payableOption) {
throw new Error('No fee option has enough balance.')
}
return { token: payableOption.feeOption.token.symbol }
}

export const wagmiConfig = createConfig({
chains: [polygon],
transports: {
Expand All @@ -37,7 +26,7 @@ export const wagmiConfig = createConfig({
networks: oms.supportedNetworks,
initialChainId: polygon.id,
transactionOptions: {
selectFeeOption: selectFirstPayableFeeOption,
selectFeeOption: FeeOptionSelector.firstAvailable,
},
}),
],
Expand Down Expand Up @@ -96,13 +85,13 @@ omsWalletConnector({
mode: TransactionMode.Relayer,
selectFeeOption: async (feeOptions) => {
const usdc = feeOptions.find(option => option.feeOption.token.symbol === 'USDC')
return usdc ? { token: usdc.feeOption.token.symbol } : undefined
return usdc?.selection
},
}),
})
```

The SDK calls `selectFeeOption` after preparing the transaction. The selector receives `FeeOptionWithBalance[]`, including each WaaS fee option and wallet balance data when the indexer can load it. Without a selector, the SDK keeps sponsored transactions fee-free and otherwise chooses the first returned fee option.
The SDK calls `selectFeeOption` after preparing the transaction. The selector receives `FeeOptionWithBalance[]`, including each WaaS fee option and wallet balance data when the indexer can load it. Use `FeeOptionSelector.firstAvailable` to choose the first option the wallet can pay, or return `option.selection` from a custom selector. Without a selector, the SDK keeps sponsored transactions fee-free and otherwise chooses the first returned fee option.

For React UI, keep `selectFeeOption` wired in the connector initializer and bridge it into a modal or sheet with app state. The workspace wagmi example shows this as a hook-driven modal; see `examples/wagmi/src/feeOptionSelectionBridge.ts`, `examples/wagmi/src/useFeeOptionSelection.ts`, and the fee option panel in `examples/wagmi/src/App.tsx`.

Expand Down
21 changes: 15 additions & 6 deletions src/clients/walletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import type {Network} from "../networks.js";
import {
FeeOptionSelector,
FeeOptionWithBalance,
feeOptionSelection,
SendContractTransactionParams,
SendDataTransactionParams, SendNativeTransactionParams,
SendTransactionParams,
Expand Down Expand Up @@ -1483,17 +1484,25 @@ export class WalletClient<Env extends OmsEnvironment = OmsEnvironment> {
network: Network
selectFeeOption?: FeeOptionSelector
}): Promise<FeeOptionSelection | undefined> {
if (params.feeOptions.length === 0) {
if (params.sponsored) {
return undefined
}

if (params.feeOptions.length === 0) {
throw new Error("No fee options available for unsponsored transaction")
}

if (!params.selectFeeOption) {
return this.defaultFeeOptionSelection(params.feeOptions, params.sponsored)
return this.defaultFeeOptionSelection(params.feeOptions)
}

return params.selectFeeOption(
const selected = await params.selectFeeOption(
await this.enrichFeeOptionsWithBalances(params.network, params.feeOptions),
)
if (!selected) {
throw new Error("No fee option selected for unsponsored transaction")
}
return selected
}

private async enrichFeeOptionsWithBalances(
Expand Down Expand Up @@ -1529,6 +1538,7 @@ export class WalletClient<Env extends OmsEnvironment = OmsEnvironment> {

return {
feeOption,
selection: feeOptionSelection(feeOption),
balance,
available: this.formatTokenAmount(balance?.balance, decimals),
availableRaw: balance?.balance,
Expand Down Expand Up @@ -1573,9 +1583,8 @@ export class WalletClient<Env extends OmsEnvironment = OmsEnvironment> {

private defaultFeeOptionSelection(
feeOptions: FeeOption[],
sponsored: boolean,
): FeeOptionSelection | undefined {
return sponsored ? undefined : feeOptions[0] ? {token: feeOptions[0].token.symbol} : undefined
): FeeOptionSelection {
return feeOptionSelection(feeOptions[0])
}

private async waitForTransactionStatus(
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ export type {
export type {
FeeOption,
FeeOptionSelection,
FeeOptionSelector,
FeeOptionWithBalance,
SendTransactionResponse,
TransactionStatusPollingOptions,
} from './types/transactionTypes.js'
export {
FeeOptionSelector,
} from './types/transactionTypes.js'
28 changes: 25 additions & 3 deletions src/types/transactionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,37 @@ export type {

export type FeeOptionWithBalance = {
feeOption: FeeOption
selection: FeeOptionSelection
balance?: TokenBalance
available?: string
availableRaw?: string
decimals?: number
}

export type FeeOptionSelector = (
feeOptions: FeeOptionWithBalance[]
) => FeeOptionSelection | undefined | Promise<FeeOptionSelection | undefined>
export interface FeeOptionSelector {
(feeOptions: FeeOptionWithBalance[]): FeeOptionSelection | undefined | Promise<FeeOptionSelection | undefined>
}

export namespace FeeOptionSelector {
export const firstAvailable: FeeOptionSelector = (feeOptions) => feeOptions.find(canPayFeeOption)?.selection
}

export function feeOptionSelection(feeOption: FeeOption): FeeOptionSelection {
const tokenIdentifier = feeOption.token.tokenID?.trim()
return {token: tokenIdentifier && tokenIdentifier.length > 0 ? tokenIdentifier : feeOption.token.symbol}
}

function canPayFeeOption(option: FeeOptionWithBalance): boolean {
if (option.availableRaw === undefined) {
return false
}

try {
return BigInt(option.availableRaw) >= BigInt(option.feeOption.value)
} catch {
return false
}
}

export type SendTransactionResponse = {
txnId: string
Expand Down
Loading
Loading