Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export const EIP712Actions: FC<EIP712ActionsProps> = ({ action, chain }) => {
tokenLogoURI={action.tokenLogoURI}
/>
);
case "eip3009.transferWithAuthorization":
// TODO: Implement EIP-3009 payment UI
return <UnknownAction typedData={action.typedData} />;
case "unknown":
return <UnknownAction typedData={action.typedData} />;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ export interface UniswapPermitSingleAction {
// typedData: TypedDataDefinition;
// }

export interface EIP3009TransferWithAuthorizationAction {
kind: "eip3009.transferWithAuthorization";
from: Address;
to: Address;
value: string | bigint;
validAfter: string | bigint;
validBefore: string | bigint;
nonce: string;
domain: EIP712Domain;
tokenLogoURI?: string;
typedData: TypedDataDefinition;
}

export interface UnknownAction {
kind: "unknown";
typedData: TypedDataDefinition;
Expand All @@ -58,5 +71,6 @@ export type EIP712Action =
| ERC2612PermitAction
| DAIPermitAction
| UniswapPermitSingleAction
| EIP3009TransferWithAuthorizationAction
// | UniswapPermitBatchAction
| UnknownAction;
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export const EthereumEip712SignatureContent: FC<
signer={payload.signer}
/>
<Spacing height={isUnknownAction ? 28 : 20} />
{action ? <EIP712Actions action={action} chain={evmChain} /> : null}
{action && evmChain ? (
<EIP712Actions action={action} chain={evmChain} />
) : null}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import type { Chain, TypedDataDefinition } from "viem";

import type {
ChainInfoForAttachedModal,
EthereumEip712SignPayload,
} from "@oko-wallet/oko-sdk-core";
import { parseTypedDataDefinition } from "@oko-wallet/oko-sdk-eth";
import type { Chain, TypedDataDefinition } from "viem";

import type {
EIP712Action,
ERC2612PermitAction,
DAIPermitAction,
UniswapPermitSingleAction,
} from "../actions/types";
import {
validateEip712Domain,
validateErc2612Permit,
validateDAIPermit,
validateUniswapPermitSingle,
validateTransferWithAuthorization,
type EIP712Domain,
} from "@oko-wallet-attached/web3/ethereum/schema";
import { findCurrencyByErc20Address } from "@oko-wallet-attached/web3/ethereum/utils";
import { useSupportedEthChain } from "@oko-wallet-attached/web3/ethereum/hooks/use_supported_eth_chain";

import type {
EIP712Action,
ERC2612PermitAction,
DAIPermitAction,
UniswapPermitSingleAction,
EIP3009TransferWithAuthorizationAction,
} from "../actions/types";

export type UseEIP712ActionResult =
| {
action: null;
Expand Down Expand Up @@ -195,6 +198,51 @@ function parseUniswapPermitSingle(
};
}

function parseEIP3009TransferWithAuthorization(
data: TypedDataDefinition,
chainInfo: ChainInfoForAttachedModal,
): EIP3009TransferWithAuthorizationAction | null {
const transferType = data.types?.TransferWithAuthorization;
if (!Array.isArray(transferType)) {
return null;
}

const message = data.message;
if (!message || typeof message !== "object") {
return null;
}

const parsed = validateTransferWithAuthorization(message);
if (!parsed.ok) {
return null;
}

if (!data.domain || typeof data.domain !== "object") {
return null;
}

const domain = parseEIP712Domain(data.domain);
if (!domain) {
return null;
}

const contractAddress = domain.verifyingContract;

return {
kind: "eip3009.transferWithAuthorization",
from: parsed.data.from,
to: parsed.data.to,
value: parsed.data.value,
validAfter: parsed.data.validAfter,
validBefore: parsed.data.validBefore,
nonce: parsed.data.nonce,
domain: domain,
typedData: data,
tokenLogoURI: findCurrencyByErc20Address(chainInfo, contractAddress)
?.coinImageUrl,
};
}

function parseAction(payload: EthereumEip712SignPayload): EIP712Action | null {
const typedData = parseTypedDataDefinition(
payload.data.serialized_typed_data,
Expand Down Expand Up @@ -225,7 +273,17 @@ function parseAction(payload: EthereumEip712SignPayload): EIP712Action | null {
break;
}

// Add more cases here for future implementations
case "TransferWithAuthorization": {
const eip3009Action = parseEIP3009TransferWithAuthorization(
typedData,
payload.chain_info,
);
if (eip3009Action) {
return eip3009Action;
}
break;
}

default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { type FC } from "react";

import type { MakeEIP712SigData } from "@oko-wallet/oko-sdk-core";
import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close";
import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
import { Button } from "@oko-wallet/oko-common-ui/button";

import styles from "@oko-wallet-attached/components/modal_variants/common/make_signature/make_signature_modal.module.scss";
import { CommonModal } from "@oko-wallet-attached/components/modal_variants/common/common_modal";
import { DemoView } from "@oko-wallet-attached/components/modal_variants/common/make_signature/demo_view";
import { ArbitrarySignatureDesc } from "@oko-wallet-attached/components/modal_variants/common/arbitrary_sig_desc/arbitrary_signature_desc";
import { SignWithOkoBox } from "@oko-wallet-attached/components/sign_with_oko_box/sign_with_oko_box";

import { useEIP712SigModal } from "./hooks/use_eip712_sig_modal";
import { useEIP712Action } from "./hooks/use_eip712_action";
import { EthereumEip712SignatureContent } from "./ethereum_eip712_signature_content";
import { SignWithOkoBox } from "@oko-wallet-attached/components/sign_with_oko_box/sign_with_oko_box";
import { X402PaymentDesc } from "./x402_payment_desc";

export const MakeEIP712SigModal: FC<MakeEIP712SigModalProps> = ({
getIsAborted,
Expand All @@ -24,6 +27,10 @@ export const MakeEIP712SigModal: FC<MakeEIP712SigModalProps> = ({
modalId,
});

const actionResult = useEIP712Action(data.payload);
const isEIP3009Payment =
actionResult.action?.kind === "eip3009.transferWithAuthorization";

return (
<div className={styles.container}>
<CommonModal className={styles.modal}>
Expand All @@ -36,7 +43,7 @@ export const MakeEIP712SigModal: FC<MakeEIP712SigModalProps> = ({
</div>

<Spacing height={20} />
<ArbitrarySignatureDesc />
{isEIP3009Payment ? <X402PaymentDesc /> : <ArbitrarySignatureDesc />}

<Spacing height={20} />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { FC } from "react";
import { Typography } from "@oko-wallet/oko-common-ui/typography";

import styles from "@oko-wallet-attached/components/modal_variants/common/arbitrary_sig_desc/arbitrary_signature_description.module.scss";

export const X402PaymentDesc: FC = () => {
return (
<Typography
size="xs"
weight="medium"
color="tertiary"
className={styles.description}
>
This will authorize a payment. Gas fees are covered by the facilitator.
</Typography>
);
};
34 changes: 34 additions & 0 deletions embed/oko_attached/src/web3/ethereum/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,37 @@ export function validateUniswapPermitSingle(
return { ok: false, error: e as ZodError };
}
}

// EIP-3009 TransferWithAuthorization (used by x402):
// from(address), to(address), value(uint256), validAfter(uint256), validBefore(uint256), nonce(bytes32)
export function getTransferWithAuthorizationSchema(opts?: BuildOptions) {
return z
.object({
from: addressSchema(),
to: addressSchema(),
value: uintSchema(opts?.uintOutput),
validAfter: uintSchema(opts?.uintOutput),
validBefore: uintSchema(opts?.uintOutput),
nonce: z.string(),
})
.strict();
}

export type TransferWithAuthorization = z.infer<
ReturnType<typeof getTransferWithAuthorizationSchema>
>;

export function validateTransferWithAuthorization(
payload: unknown,
opts?: BuildOptions,
):
| { ok: true; data: TransferWithAuthorization }
| { ok: false; error: ZodError } {
const schema = getTransferWithAuthorizationSchema(opts);
try {
const parsed = schema.parse(payload);
return { ok: true, data: parsed };
} catch (e) {
return { ok: false, error: e as ZodError };
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"crypto/teddsa/teddsa_interface",
"crypto/teddsa/api_lib",
"crypto/teddsa/frost_ed25519_keplr_wasm",
"sandbox/sandbox_sol"
"sandbox/sandbox_sol",
"sandbox/sandbox_x402"
]
},
"packageManager": "yarn@4.10.3",
Expand Down
8 changes: 8 additions & 0 deletions sandbox/sandbox_x402/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Oko SDK Configuration
NEXT_PUBLIC_OKO_API_KEY=your_oko_api_key_here
NEXT_PUBLIC_OKO_SDK_ENDPOINT=https://sdk.okowallet.com

# x402 Server Configuration
NEXT_PUBLIC_X402_SERVER_URL=http://localhost:4402
PAYMENT_ADDRESS=0x_your_payment_receiver_address_here
PORT=4402
5 changes: 5 additions & 0 deletions sandbox/sandbox_x402/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
.env
.env.local
*.log
Loading