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
199 changes: 199 additions & 0 deletions packages/controller/src/__tests__/toWasmPolicies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,203 @@ describe("toWasmPolicies", () => {
expect(result).toEqual([]);
});
});

describe("ApprovalPolicy handling", () => {
test("creates ApprovalPolicy for approve methods with spender and amount", () => {
const policies: ParsedSessionPolicies = {
verified: false,
contracts: {
"0xTOKEN": {
methods: [
{
entrypoint: "approve",
spender: "0xSPENDER",
amount: "1000000000000000000",
authorized: true,
},
],
},
},
};

const result = toWasmPolicies(policies);

expect(result).toHaveLength(1);
expect(result[0]).toEqual({
target: "0xTOKEN",
spender: "0xSPENDER",
amount: "1000000000000000000",
});
// Should NOT have method or authorized fields
expect(result[0]).not.toHaveProperty("method");
expect(result[0]).not.toHaveProperty("authorized");
});

test("converts numeric amount to string in ApprovalPolicy", () => {
const policies: ParsedSessionPolicies = {
verified: false,
contracts: {
"0xTOKEN": {
methods: [
{
entrypoint: "approve",
spender: "0xSPENDER",
amount: 1000000000000000000,
authorized: true,
},
],
},
},
};

const result = toWasmPolicies(policies);

expect(result[0]).toHaveProperty("amount", "1000000000000000000");
});

test("falls back to CallPolicy for approve without spender", () => {
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});

const policies: ParsedSessionPolicies = {
verified: false,
contracts: {
"0xTOKEN": {
methods: [
{
entrypoint: "approve",
authorized: true,
},
],
},
},
};

const result = toWasmPolicies(policies);

expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty("method");
expect(result[0]).toHaveProperty("authorized", true);
expect(result[0]).not.toHaveProperty("spender");
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("[DEPRECATED]"),
);

warnSpy.mockRestore();
});

test("falls back to CallPolicy for approve without amount", () => {
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});

const policies: ParsedSessionPolicies = {
verified: false,
contracts: {
"0xTOKEN": {
methods: [
{
entrypoint: "approve",
spender: "0xSPENDER",
authorized: true,
},
],
},
},
};

const result = toWasmPolicies(policies);

expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty("method");
expect(result[0]).not.toHaveProperty("spender");
expect(warnSpy).toHaveBeenCalled();

warnSpy.mockRestore();
});

test("creates CallPolicy for non-approve methods", () => {
const policies: ParsedSessionPolicies = {
verified: false,
contracts: {
"0xCONTRACT": {
methods: [
{
entrypoint: "transfer",
authorized: true,
},
],
},
},
};

const result = toWasmPolicies(policies);

expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty("target", "0xCONTRACT");
expect(result[0]).toHaveProperty("method");
expect(result[0]).toHaveProperty("authorized", true);
});

test("handles mixed approve and non-approve methods", () => {
const policies: ParsedSessionPolicies = {
verified: false,
contracts: {
"0xTOKEN": {
methods: [
{
entrypoint: "approve",
spender: "0xSPENDER",
amount: "1000",
authorized: true,
},
{
entrypoint: "transfer",
authorized: true,
},
],
},
},
};

const result = toWasmPolicies(policies);

expect(result).toHaveLength(2);

// First should be approve (sorted alphabetically)
const approvePolicy = result[0];
expect(approvePolicy).toHaveProperty("spender", "0xSPENDER");
expect(approvePolicy).toHaveProperty("amount", "1000");

// Second should be transfer
const transferPolicy = result[1];
expect(transferPolicy).toHaveProperty("method");
expect(transferPolicy).toHaveProperty("authorized", true);
});

test("sorts approve policies correctly among other methods", () => {
const policies: ParsedSessionPolicies = {
verified: false,
contracts: {
"0xTOKEN": {
methods: [
{ entrypoint: "transfer", authorized: true },
{
entrypoint: "approve",
spender: "0xSPENDER",
amount: "1000",
authorized: true,
},
{ entrypoint: "balance_of", authorized: true },
],
},
},
};

const result = toWasmPolicies(policies);

expect(result).toHaveLength(3);
// Sorted order: approve, balance_of, transfer
expect(result[0]).toHaveProperty("spender"); // approve -> ApprovalPolicy
expect(result[1]).toHaveProperty("method"); // balance_of -> CallPolicy
expect(result[2]).toHaveProperty("method"); // transfer -> CallPolicy
});
});
});
3 changes: 2 additions & 1 deletion packages/controller/src/policies.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Approval,
ContractPolicy,
Method,
SessionPolicies,
Expand All @@ -14,7 +15,7 @@ export type ParsedSessionPolicies = {
export type SessionContracts = Record<
string,
Omit<ContractPolicy, "methods"> & {
methods: (Method & { authorized?: boolean })[];
methods: ((Method | Approval) & { authorized?: boolean })[];
}
>;

Expand Down
34 changes: 28 additions & 6 deletions packages/controller/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Policy } from "@cartridge/controller-wasm/controller";
import { Policy, ApprovalPolicy } from "@cartridge/controller-wasm/controller";
import { Policies, SessionPolicies } from "@cartridge/presets";
import { ChainId } from "@starknet-io/types-js";
import {
Expand Down Expand Up @@ -105,11 +105,33 @@ export function toWasmPolicies(policies: ParsedSessionPolicies): Policy[] {
toArray(methods)
.slice()
.sort((a, b) => a.entrypoint.localeCompare(b.entrypoint))
.map((m) => ({
target,
method: hash.getSelectorFromName(m.entrypoint),
authorized: m.authorized,
})),
.map((m): Policy => {
// Check if this is an approve entrypoint with spender and amount
if (m.entrypoint === "approve") {
if ("spender" in m && "amount" in m && m.spender && m.amount) {
const approvalPolicy: ApprovalPolicy = {
target,
spender: m.spender,
amount: String(m.amount),
};
return approvalPolicy;
}

// Fall back to CallPolicy with deprecation warning
console.warn(
`[DEPRECATED] Approve method without spender and amount fields will be rejected in future versions. ` +
`Please update your preset or policies to include both 'spender' and 'amount' fields for approve calls on contract ${target}. ` +
`Example: { entrypoint: "approve", spender: "0x...", amount: "0x..." }`,
);
}

// For non-approve methods and legacy approve, create a regular CallPolicy
return {
target,
method: hash.getSelectorFromName(m.entrypoint),
authorized: m.authorized,
};
}),
),
...(policies.messages ?? [])
.map((p) => {
Expand Down
Loading