Skip to content
Closed
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,8 @@
"lib/",
"networks.json",
"src/"
]
],
"dependencies": {
"viem": "^2.29.2"
}
}
52 changes: 52 additions & 0 deletions src/ts/adapters/ethereum-client-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Address, TypedDataDomain } from "../types/core";
import type { Order, OrderSignature } from "../types/order";
import type {
SignerContext,
TypedDataTypes,
TypedDataValue,
} from "../types/signing";

export interface EthereumClientAdapter {
// Address operations
getAddress(address: string): Address;

// Hashing operations
keccak256(data: Uint8Array): string;
hashTypedData(
domain: TypedDataDomain,
types: TypedDataTypes,
data: TypedDataValue,
): string;

// Amount formatting
formatAmount(amount: bigint): string;
parseAmount(amount: string): bigint;

// Order operations
hashOrder(domain: TypedDataDomain, order: Order): string;
signOrder(
domain: TypedDataDomain,
order: Order,
signer: SignerContext,
): Promise<OrderSignature>;

// Data conversion
arrayify(hex: string): Uint8Array;
hexlify(bytes: Uint8Array): string;

// Signature handling
joinSignature(
signature: string | { r: string; s: string; v: number },
): string;
encodeEip1271SignatureData(data: {
verifier: string;
signature: string | Uint8Array;
}): string;

// Function encoding
encodeFunction(
abi: Array<{ name: string; inputs?: Array<{ type: string }> }>,
functionName: string,
args: unknown[],
): string;
}
136 changes: 136 additions & 0 deletions src/ts/adapters/ethers-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// src/ts/adapters/ethers-adapter.ts
import { ethers } from "ethers";
import { ORDER_TYPE_FIELDS } from "../constants";
import type { NormalizedOrder } from "../order";
import type { Address, TypedDataDomain } from "../types/core";
import type { Order, OrderSignature } from "../types/order";
import {
type SignerContext,
type SignerDomain,
SigningScheme,
type TypedDataTypes,
type TypedDataValue,
} from "../types/signing";
import type { EthereumClientAdapter } from "./ethereum-client-adapter";

export class EthersAdapter implements EthereumClientAdapter {
getAddress(address: string): Address {
return { value: ethers.utils.getAddress(address) };
}

keccak256(data: Uint8Array): string {
return ethers.utils.keccak256(data);
}

hashTypedData(
domain: TypedDataDomain,
types: TypedDataTypes,
data: TypedDataValue,
): string {
// Convert domain to ethers format
const ethersDomain: SignerDomain = {
name: domain.name,
version: domain.version,
chainId: domain.chainId,
verifyingContract: domain.verifyingContract,
};

return ethers.utils._TypedDataEncoder.hash(ethersDomain, types, data);
}

formatAmount(amount: bigint): string {
return amount.toString();
}

parseAmount(amount: string): bigint {
return BigInt(amount);
}

arrayify(hex: string): Uint8Array {
return ethers.utils.arrayify(hex);
}

hexlify(bytes: Uint8Array): string {
return ethers.utils.hexlify(bytes);
}

joinSignature(
signature: string | { r: string; s: string; v: number },
): string {
return ethers.utils.joinSignature(signature);
}

encodeEip1271SignatureData(data: {
verifier: string;
signature: string | Uint8Array;
}): string {
return ethers.utils.solidityPack(
["address", "bytes"],
[data.verifier, data.signature],
);
}

encodeFunction(
abi: Array<{ name: string; inputs?: Array<{ type: string }> }>,
functionName: string,
args: unknown[],
): string {
const iface = new ethers.utils.Interface(
abi.map((fn) => {
const inputs = fn.inputs
? `(${fn.inputs.map((i) => i.type).join(",")})`
: "()";
return `function ${fn.name}${inputs}`;
}),
);
return iface.encodeFunctionData(functionName, args);
}

hashOrder(domain: TypedDataDomain, order: Order): string {
const normalizedOrder = this.normalizeOrder(order);
return this.hashTypedData(
domain,
{ Order: ORDER_TYPE_FIELDS },
normalizedOrder,
);
}

async signOrder(
domain: TypedDataDomain,
order: Order,
signer: SignerContext,
): Promise<OrderSignature> {
const normalizedOrder = this.normalizeOrder(order);

// Convert domain to signer format
const signerDomain: SignerDomain = {
name: domain.name,
version: domain.version,
chainId: domain.chainId,
verifyingContract: domain.verifyingContract,
};

const signature = await signer.signTypedData(
signerDomain,
{ Order: ORDER_TYPE_FIELDS },
normalizedOrder,
);

return {
scheme: SigningScheme.EIP712,
data: signature,
};
}

private normalizeOrder(order: Order): NormalizedOrder {
return {
...order,
receiver: order.receiver || ethers.constants.AddressZero,
sellTokenBalance: order.sellTokenBalance || "erc20",
buyTokenBalance: order.buyTokenBalance || "erc20",
sellAmount: order.sellAmount,
buyAmount: order.buyAmount,
feeAmount: order.feeAmount,
};
}
}
162 changes: 162 additions & 0 deletions src/ts/adapters/viem-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// src/ts/adapters/viem-adapter.ts
import {
encodeFunctionData,
fromHex,
parseAbiItem,
toHex,
getAddress as viemGetAddress,
hashTypedData as viemHashTypedData,
keccak256 as viemKeccak256,
} from "viem";
import { ORDER_TYPE_FIELDS } from "../constants";
import type { Address, TypedDataDomain } from "../types/core";
import type { Order, OrderSignature } from "../types/order";
import {
type SignerContext,
type SignerDomain,
SigningScheme,
type TypedDataTypes,
type TypedDataValue,
} from "../types/signing";
import type { EthereumClientAdapter } from "./ethereum-client-adapter";

export class ViemAdapter implements EthereumClientAdapter {
getAddress(address: string): Address {
return { value: viemGetAddress(address as `0x${string}`) };
}

keccak256(data: Uint8Array): string {
return viemKeccak256(data);
}

hashTypedData(
domain: TypedDataDomain,
types: TypedDataTypes,
data: TypedDataValue,
): string {
const primaryType = Object.keys(types)[0];

return viemHashTypedData({
domain,
primaryType,
types,
message: data,
});
}

formatAmount(amount: bigint): string {
return amount.toString();
}

parseAmount(amount: string): bigint {
return BigInt(amount);
}

arrayify(hex: string): Uint8Array {
return fromHex(hex as `0x${string}`, "bytes");
}

hexlify(bytes: Uint8Array): string {
return toHex(bytes);
}

joinSignature(
signature: string | { r: string; s: string; v: number },
): string {
// For viem, if it's already a string, return it as is
if (typeof signature === "string") {
return signature;
}

// Otherwise construct the signature string from r, s, v
const { r, s, v } = signature;
// Ensure v is properly formatted as a single byte
const vByte = v < 27 ? v + 27 : v;
return `${r}${s.slice(2)}${vByte.toString(16).padStart(2, "0")}`;
}

encodeEip1271SignatureData(data: {
verifier: string;
signature: string | Uint8Array;
}): string {
// Convert signature to hex string if it's a Uint8Array
const sigHex =
typeof data.signature === "string"
? data.signature
: toHex(data.signature);

// Concatenate the address and signature
return `${this.getAddress(data.verifier).value}${sigHex.startsWith("0x") ? sigHex.slice(2) : sigHex}`;
}

encodeFunction(
abi: Array<{ name: string; inputs?: Array<{ type: string }> }>,
functionName: string,
args: unknown[],
): string {
// Find the function in the ABI
const funcAbi = abi.find((fn) => fn.name === functionName);
if (!funcAbi) {
throw new Error(`Function ${functionName} not found in ABI`);
}

// Create the function signature
const inputs = funcAbi.inputs || [];
const signature = `${functionName}(${inputs.map((i) => i.type).join(",")})`;

// Encode the function data
return encodeFunctionData({
abi: [parseAbiItem(`function ${signature}`)],
functionName,
args,
});
}

hashOrder(domain: TypedDataDomain, order: Order): string {
const normalizedOrder = this.normalizeOrder(order);
return this.hashTypedData(
domain,
{ Order: ORDER_TYPE_FIELDS },
normalizedOrder,
);
}

async signOrder(
domain: TypedDataDomain,
order: Order,
signer: SignerContext,
): Promise<OrderSignature> {
const normalizedOrder = this.normalizeOrder(order);

// Convert domain to signer format
const signerDomain: SignerDomain = {
name: domain.name,
version: domain.version,
chainId: domain.chainId,
verifyingContract: domain.verifyingContract,
};

const signature = await signer.signTypedData(
signerDomain,
{ Order: ORDER_TYPE_FIELDS },
normalizedOrder,
);

return {
scheme: SigningScheme.EIP712,
data: signature,
};
}

private normalizeOrder(order: Order) {
return {
...order,
receiver: order.receiver || "0x0000000000000000000000000000000000000000",
sellTokenBalance: order.sellTokenBalance || "erc20",
buyTokenBalance: order.buyTokenBalance || "erc20",
sellAmount: order.sellAmount.toString(),
buyAmount: order.buyAmount.toString(),
feeAmount: order.feeAmount.toString(),
};
}
}
17 changes: 17 additions & 0 deletions src/ts/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* The EIP-712 type fields definition for a Gnosis Protocol v2 order.
*/
export const ORDER_TYPE_FIELDS = [
{ name: "sellToken", type: "address" },
{ name: "buyToken", type: "address" },
{ name: "receiver", type: "address" },
{ name: "sellAmount", type: "uint256" },
{ name: "buyAmount", type: "uint256" },
{ name: "validTo", type: "uint32" },
{ name: "appData", type: "bytes32" },
{ name: "feeAmount", type: "uint256" },
{ name: "kind", type: "string" },
{ name: "partiallyFillable", type: "bool" },
{ name: "sellTokenBalance", type: "string" },
{ name: "buyTokenBalance", type: "string" },
];
Loading
Loading