Skip to content

Commit 7e34cf7

Browse files
matt416claude
andcommitted
feat(dapp-client): add isSponsored for explicit sponsorship checks
`DappClient.isSponsored(chainId, transactions)` and `ChainSessionManager.isSponsored(calls)` return true only when the relayer's `/FeeOptions` endpoint explicitly reports sponsorship; any error, network failure, or absence of sponsorship returns false. A true result is always safe to surface as "free gas" in UI. Prefer this over inferring sponsorship from an empty `getFeeOptions` array — a swallowed `/FeeOptions` error produces the same empty shape as a real subsidy. `getFeeOptions` is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 583aef0 commit 7e34cf7

3 files changed

Lines changed: 91 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@0xsequence/dapp-client': minor
3+
---
4+
5+
Add `isSponsored` for explicit sponsorship checks.
6+
7+
`DappClient.isSponsored(chainId, transactions)` and
8+
`ChainSessionManager.isSponsored(calls)` return `true` only when the relayer's
9+
`/FeeOptions` endpoint explicitly reports sponsorship. Any error, network
10+
failure, or absence of sponsorship returns `false`, so a `true` result is
11+
always safe to surface as "free gas" in UI.
12+
13+
Prefer this over inferring sponsorship from an empty `getFeeOptions` array — a
14+
swallowed `/FeeOptions` error also produces an empty array, so the inference
15+
can misclassify a failed quote as a real subsidy. The new method uses the
16+
positive `sponsored: boolean` signal from `@0xsequence/relayer`'s widened
17+
`feeOptions` return.
18+
19+
`getFeeOptions` is unchanged.

packages/wallet/dapp-client/src/ChainSessionManager.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,54 @@ export class ChainSessionManager {
878878
}
879879
}
880880

881+
/**
882+
* Checks whether the given transactions would be sponsored by an active
883+
* relayer policy on this chain.
884+
*
885+
* Returns `true` only when the relayer's `/FeeOptions` endpoint explicitly
886+
* reports sponsorship. A failed quote, network error, or absence of
887+
* sponsorship all return `false`, so a `true` result is always safe to
888+
* surface as "free gas" in UI.
889+
*/
890+
async isSponsored(calls: Transaction[]): Promise<boolean> {
891+
const callsToSend = calls.map((tx) => ({
892+
to: tx.to,
893+
value: tx.value,
894+
data: tx.data,
895+
gasLimit: tx.gasLimit ?? BigInt(0),
896+
delegateCall: tx.delegateCall ?? false,
897+
onlyFallback: tx.onlyFallback ?? false,
898+
behaviorOnError: tx.behaviorOnError ?? ('revert' as const),
899+
}))
900+
try {
901+
const signedCall = await this._buildAndSignCalls(callsToSend)
902+
const fingerprint = this._fingerprintCalls(callsToSend)
903+
if (fingerprint) {
904+
this.lastSignedCallCache = {
905+
fingerprint,
906+
signedCall,
907+
createdAtMs: Date.now(),
908+
}
909+
}
910+
const walletAddress = this.walletAddress
911+
if (!walletAddress) throw new InitializationError('Wallet is not initialized.')
912+
const feeOptions = await this.relayer.feeOptions(
913+
walletAddress,
914+
this.chainId,
915+
signedCall.to,
916+
callsToSend,
917+
signedCall.data,
918+
)
919+
return feeOptions.sponsored === true && !feeOptions.failed
920+
} catch (err) {
921+
console.warn(
922+
`isSponsored check failed for chain ${this.chainId}:`,
923+
err instanceof Error ? err.message : String(err),
924+
)
925+
return false
926+
}
927+
}
928+
881929
/**
882930
* Builds, signs, and sends a batch of transactions.
883931
* @param transactions The transactions to be sent.

packages/wallet/dapp-client/src/DappClient.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,30 @@ export class DappClient {
806806
return await chainSessionManager.getFeeOptions(transactions)
807807
}
808808

809+
/**
810+
* Checks whether the given transactions would be sponsored on `chainId`.
811+
*
812+
* Returns `true` only when the relayer's `/FeeOptions` endpoint explicitly
813+
* reports sponsorship. A failed quote, network error, or absence of
814+
* sponsorship all return `false`, so a `true` result is always safe to
815+
* surface as "free gas" in UI.
816+
*
817+
* Prefer this over inferring sponsorship from an empty `getFeeOptions`
818+
* array — a swallowed `/FeeOptions` error also produces an empty array.
819+
*
820+
* @example
821+
* if (await dappClient.isSponsored(1, transactions)) {
822+
* // safe to show "Free gas, sponsored by app"
823+
* } else {
824+
* const feeOptions = await dappClient.getFeeOptions(1, transactions)
825+
* // present feeOptions[0..n] to the user as payment choices
826+
* }
827+
*/
828+
async isSponsored(chainId: number, transactions: Transaction[]): Promise<boolean> {
829+
const chainSessionManager = await this.getOrInitializeChainManager(chainId)
830+
return await chainSessionManager.isSponsored(transactions)
831+
}
832+
809833
/**
810834
* Fetches fee tokens for a chain.
811835
* @returns A promise that resolves with the fee tokens response. {@link GetFeeTokensResponse}

0 commit comments

Comments
 (0)