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
2 changes: 1 addition & 1 deletion examples/flappy-bird/starknet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async function executeGameCall(
calldata: Calldata = []
): Promise<void> {
if (!wallet) return;
const feeMode = useUserPaysForSession ? "user_pays" : "sponsored";
const feeMode = useUserPaysForSession ? ("user_pays" as const) : ({ type: "paymaster" } as const);
try {
await wallet.execute(
[
Expand Down
16 changes: 8 additions & 8 deletions packages/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1413,16 +1413,16 @@ async function handleTool(
maxBatchAmount
);

const feeMode: "sponsored" | undefined = parsed.sponsored
? "sponsored"
const feeMode = parsed.sponsored
? ({ type: "paymaster" } as const)
: undefined;
const tx = await withTimeout("Token transfer submission", () =>
wallet.transfer(token, transfers, {
...(feeMode && { feeMode }),
})
);
const txResult = await waitForTrackedTransaction(tx);
if (feeMode === "sponsored") {
if (feeMode) {
await assertWalletAccountClassHash(
wallet,
"Sponsored transfer post-check"
Expand Down Expand Up @@ -1451,16 +1451,16 @@ async function handleTool(
entrypoint: call.entrypoint,
calldata: call.calldata ?? [],
}));
const feeMode: "sponsored" | undefined = parsed.sponsored
? "sponsored"
const feeMode = parsed.sponsored
? ({ type: "paymaster" } as const)
: undefined;
const tx = await withTimeout("Contract execution submission", () =>
wallet.execute(calls, {
...(feeMode && { feeMode }),
})
);
const txResult = await waitForTrackedTransaction(tx);
if (feeMode === "sponsored") {
if (feeMode) {
await assertWalletAccountClassHash(
wallet,
"Sponsored execute post-check"
Expand Down Expand Up @@ -1501,8 +1501,8 @@ async function handleTool(
address: wallet.address,
});
}
const feeMode: "sponsored" | undefined = parsed.sponsored
? "sponsored"
const feeMode = parsed.sponsored
? ({ type: "paymaster" } as const)
: undefined;
const tx = await withTimeout("Account deployment submission", () =>
wallet.deploy({
Expand Down
38 changes: 30 additions & 8 deletions packages/native/src/wallet/cartridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,21 @@ function unsupportedSessionFeature(feature: string): Error {
}

function unsupportedUserPaysMessage(): string {
return 'Cartridge wallet currently supports sponsored session execution only. Use feeMode: "sponsored".';
return 'Cartridge wallet currently supports sponsored session execution only. Use feeMode: { type: "paymaster" }.';
}

export type SupportedNativeCartridgeFeeMode = Extract<FeeMode, "sponsored">;
function unsupportedGasTokenMessage(): string {
return 'Cartridge wallet does not support gasToken. Use feeMode: { type: "paymaster" } without gasToken.';
}

/**
* Fee modes supported by native Cartridge sessions.
* Only sponsored execution is supported — `gasToken` is not available.
*/
export type SupportedNativeCartridgeFeeMode =
| "sponsored"
| { type: "paymaster" };

type UniversalDetailsWithTimeBounds = UniversalDetails & {
timeBounds?: PaymasterTimeBounds;
};
Expand All @@ -85,6 +96,16 @@ export function validateSupportedCartridgeFeeMode(
if (feeMode === undefined || feeMode === "sponsored") {
return feeMode;
}
if (
typeof feeMode === "object" &&
feeMode !== null &&
feeMode.type === "paymaster"
) {
if (feeMode.gasToken) {
throw new Error(unsupportedGasTokenMessage());
}
return { type: "paymaster" };
}

throw new Error(unsupportedUserPaysMessage());
}
Expand Down Expand Up @@ -273,7 +294,7 @@ export class NativeCartridgeWallet extends BaseWallet {
this.chainId = options.chainId;
this.classHash = options.classHash;
this.explorerConfig = options.explorer;
this.defaultFeeMode = options.feeMode ?? "sponsored";
this.defaultFeeMode = options.feeMode ?? { type: "paymaster" };
this.defaultTimeBounds = options.timeBounds;
this.account = new NativeCartridgeAccount({
session: options.session,
Expand All @@ -285,8 +306,9 @@ export class NativeCartridgeWallet extends BaseWallet {
static async create(
options: NativeCartridgeWalletOptions
): Promise<NativeCartridgeWallet> {
const feeMode =
validateSupportedCartridgeFeeMode(options.feeMode) ?? "sponsored";
const feeMode = validateSupportedCartridgeFeeMode(options.feeMode) ?? {
type: "paymaster",
};
let classHash: string | undefined;
try {
classHash = await options.provider.getClassHashAt(
Expand All @@ -301,7 +323,7 @@ export class NativeCartridgeWallet extends BaseWallet {
return new NativeCartridgeWallet({
...options,
...(classHash !== undefined && { classHash }),
...(feeMode && { feeMode }),
feeMode,
});
}

Expand Down Expand Up @@ -358,7 +380,7 @@ export class NativeCartridgeWallet extends BaseWallet {

async execute(calls: Call[], options: ExecuteOptions = {}): Promise<Tx> {
const feeMode = options.feeMode ?? this.defaultFeeMode;
if (feeMode !== "sponsored") {
if (feeMode === "user_pays") {
throw new Error(unsupportedUserPaysMessage());
}
const timeBounds = options.timeBounds ?? this.defaultTimeBounds;
Expand All @@ -384,7 +406,7 @@ export class NativeCartridgeWallet extends BaseWallet {

async preflight(options: PreflightOptions): Promise<PreflightResult> {
const feeMode = options.feeMode ?? this.defaultFeeMode;
if (feeMode !== "sponsored") {
if (feeMode === "user_pays") {
return { ok: false, reason: unsupportedUserPaysMessage() };
}
const simulate = this.session.account.simulateTransaction;
Expand Down
12 changes: 6 additions & 6 deletions packages/native/tests/native-cartridge-wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ describe("NativeCartridgeWallet", () => {
});

const tx = await wallet.execute([{ contractAddress: "0x1" } as Call], {
feeMode: "sponsored",
feeMode: { type: "paymaster" },
});

expect(tx.hash).toBe("0xfeed");
expect(wallet.getFeeMode()).toBe("sponsored");
expect(wallet.getFeeMode()).toEqual({ type: "paymaster" });
expect(session.account.execute).toHaveBeenCalledTimes(1);
expect(wallet.getAccount()).toBeInstanceOf(Account);
});
Expand All @@ -72,7 +72,7 @@ describe("NativeCartridgeWallet", () => {

const err = await wallet
.execute([{ contractAddress: "0x1" } as Call], {
feeMode: "sponsored",
feeMode: { type: "paymaster" },
})
.then(
() => {
Expand Down Expand Up @@ -124,7 +124,7 @@ describe("NativeCartridgeWallet", () => {
});

it("rejects unsupported default fee mode during creation", async () => {
const unsupportedFeeMode = "user_pays" as unknown as "sponsored";
const unsupportedFeeMode = "user_pays" as const;

await expect(
NativeCartridgeWallet.create({
Expand Down Expand Up @@ -185,7 +185,7 @@ describe("NativeCartridgeWallet", () => {

await expect(
wallet.execute([{ contractAddress: "0x1" } as Call], {
feeMode: "sponsored",
feeMode: { type: "paymaster" },
})
).resolves.toMatchObject({ hash: "0xfeed" });

Expand Down Expand Up @@ -228,7 +228,7 @@ describe("NativeCartridgeWallet", () => {
await expect(
wallet.preflight({
calls: [{ contractAddress: "0x1" } as Call],
feeMode: "sponsored",
feeMode: { type: "paymaster" },
})
).resolves.toEqual({
ok: true,
Expand Down
2 changes: 1 addition & 1 deletion src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export class StarkZap {
* // With sponsored transactions
* const wallet = await sdk.connectWallet({
* account: { signer: new StarkSigner(privateKey) },
* feeMode: "sponsored",
* feeMode: { type: "paymaster" },
* });
* ```
*/
Expand Down
47 changes: 31 additions & 16 deletions src/types/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,21 @@ export interface AccountConfig {

/**
* How transaction fees are paid.
* - `"sponsored"`: Paymaster covers gas
* - `"user_pays"`: User's account pays gas in ETH/STRK
*
* - `"user_pays"` — User's account pays gas in ETH/STRK
* - `{ type: "paymaster" }` — Paymaster covers gas (sponsored)
* - `{ type: "paymaster", gasToken: "0x..." }` — Pay gas via ERC-20 through paymaster
* - `"sponsored"` — *(deprecated)* Alias for `{ type: "paymaster" }`
*/
export type FeeMode =
| "user_pays"
| { type: "paymaster"; gasToken?: Address }
| DeprecatedSponsoredFeeMode;

/**
* @deprecated Use `{ type: "paymaster" }` instead.
*/
export type FeeMode = "sponsored" | "user_pays";
type DeprecatedSponsoredFeeMode = "sponsored";

// ─── Provider Options ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -109,7 +120,7 @@ export interface ProviderOptions {
* // Sponsored via AVNU paymaster
* await sdk.connectWallet({
* account: { signer: new StarkSigner(privateKey) },
* feeMode: "sponsored",
* feeMode: { type: "paymaster" },
* });
* ```
*/
Expand Down Expand Up @@ -154,7 +165,7 @@ export interface ProgressEvent {
* ```ts
* await wallet.ensureReady({
* deploy: "if_needed",
* feeMode: "sponsored",
* feeMode: { type: "paymaster" },
* onProgress: (e) => console.log(e.step)
* });
* ```
Expand All @@ -168,25 +179,25 @@ export interface EnsureReadyOptions {
onProgress?: (event: ProgressEvent) => void;
}

// ─── Deploy ──────────────────────────────────────────────────────────────────
// ─── Transaction Fee Options ─────────────────────────────────────────────────

/** Options for `wallet.deploy()` */
export interface DeployOptions {
/** Common fee options shared by deploy and execute operations. */
interface TransactionFeeOptions {
/** How fees are paid (default: "user_pays") */
feeMode?: FeeMode;
/** Optional time bounds for paymaster-sponsored deployment */
/** Optional time bounds for paymaster transactions */
timeBounds?: PaymasterTimeBounds;
}

// ─── Deploy ──────────────────────────────────────────────────────────────────

/** Options for `wallet.deploy()` */
export type DeployOptions = TransactionFeeOptions;

// ─── Execute ─────────────────────────────────────────────────────────────────

/** Options for `wallet.execute()` */
export interface ExecuteOptions {
/** How fees are paid */
feeMode?: FeeMode;
/** Optional time bounds for paymaster transactions */
timeBounds?: PaymasterTimeBounds;
}
export type ExecuteOptions = TransactionFeeOptions;

// ─── Preflight ───────────────────────────────────────────────────────────────

Expand All @@ -200,8 +211,12 @@ export interface PreflightOptions {
/**
* Fee mode used for preflight assumptions.
*
* When `"sponsored"` and the account is undeployed, preflight returns `{ ok: true }`
* When using a paymaster mode (`{ type: "paymaster" }` — with or without
* `gasToken`) and the account is undeployed, preflight returns `{ ok: true }`
* because the paymaster path can deploy + execute atomically.
*
* The `gasToken` field only affects which token is used for fee payment;
* it does not change the preflight deployment decision.
*/
feeMode?: FeeMode;
}
Expand Down
9 changes: 5 additions & 4 deletions src/wallet/cartridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import {
import {
checkDeployed,
ensureWalletReady,
normalizeFeeMode,
paymasterDetails,
preflightTransaction,
sponsoredDetails,
} from "@/wallet/utils";
import { BaseWallet } from "@/wallet/base";
import { assertSafeHttpUrl } from "@/utils";
Expand Down Expand Up @@ -307,18 +308,18 @@ export class CartridgeWallet extends BaseWallet {
}

async execute(calls: Call[], options: ExecuteOptions = {}): Promise<Tx> {
const feeMode = options.feeMode ?? this.defaultFeeMode;
const feeMode = normalizeFeeMode(options.feeMode ?? this.defaultFeeMode);
const timeBounds = options.timeBounds ?? this.defaultTimeBounds;

let transaction_hash: string;

if (feeMode === "sponsored") {
if (feeMode !== "user_pays") {
// Allow provider/controller implementations to handle undeployed accounts
// atomically via paymaster flow when supported.
transaction_hash = (
await this.walletAccount.executePaymasterTransaction(
calls,
sponsoredDetails(timeBounds)
paymasterDetails({ feeMode, timeBounds })
)
).transaction_hash;
} else {
Expand Down
Loading
Loading