Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explain SignRequest #108

Merged
merged 7 commits into from
Nov 14, 2024
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
155 changes: 155 additions & 0 deletions src/decode/explain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Network } from "near-ca";

import { decodeTxData } from ".";
import { DecodedTxData, SafeEncodedSignRequest } from "../types";

/**
* Explain a Safe Signature Request.
* @param signRequest - The Safe Signature Request to explain.
* @returns The decoded transaction data as stringified JSON or null if there was an error.
*/
export async function explainSignRequest(
signRequest: SafeEncodedSignRequest
): Promise<string> {
// Decode the Signature Request
const decodedEvmData = decodeTxData(signRequest);

// Decode the function signatures
const functionSignatures = await Promise.all(
decodedEvmData.transactions.map((tx) =>
safeDecodeTx(tx.data, tx.to, decodedEvmData.chainId)
)
);

// Format the decoded data
return formatEvmData(decodedEvmData, functionSignatures);
}

const SAFE_NETWORKS: { [chainId: number]: string } = {
1: "mainnet", // Ethereum Mainnet
10: "optimism", // Optimism Mainnet
56: "binance", // Binance Smart Chain Mainnet
97: "bsc-testnet", // Binance Smart Chain Testnet
100: "gnosis-chain", // Gnosis Chain (formerly xDAI)
137: "polygon", // Polygon Mainnet
250: "fantom", // Fantom Mainnet
288: "boba", // Boba Network Mainnet
1284: "moonbeam", // Moonbeam (Polkadot)
1285: "moonriver", // Moonriver (Kusama)
4002: "fantom-testnet", // Fantom Testnet
42161: "arbitrum", // Arbitrum One Mainnet
43113: "avalanche-fuji", // Avalanche Fuji Testnet
43114: "avalanche", // Avalanche Mainnet
80001: "polygon-mumbai", // Polygon Mumbai Testnet
8453: "base", // Base Mainnet
11155111: "sepolia", // Sepolia Testnet
1666600000: "harmony", // Harmony Mainnet
1666700000: "harmony-testnet", // Harmony Testnet
1313161554: "aurora", // Aurora Mainnet (NEAR)
1313161555: "aurora-testnet", // Aurora Testnet (NEAR)
};

/**
* Represents a parameter in a decoded contract call.
*/
interface DecodedParameter {
/** The parameter name from the contract ABI */
name: string;
/** The parameter type (e.g., 'address', 'uint256') */
type: string;
/** The actual value of the parameter */
value: string;
}

/**
* Represents a successful response from the Safe transaction decoder.
*/
interface FunctionSignature {
/** The name of the contract method that was called */
method: string;
/** Array of decoded parameters from the function call */
parameters: DecodedParameter[];
}

/**
* Represents an error response from the Safe transaction decoder.
*/
interface SafeDecoderErrorResponse {
/** Error code from the Safe API */
code: number;
/** Human-readable error message */
message: string;
/** Additional error context arguments */
arguments: string[];
}

/**
* Decode a transaction using the Safe Decoder API. According to this spec:
* https://safe-transaction-sepolia.safe.global/#/data-decoder/data_decoder_create
* @param data - The transaction data to decode.
* @param to - The address of the contract that was called.
* @param chainId - The chain ID of the transaction.
* @returns The decoded transaction data or null if there was an error.
*/
export async function safeDecodeTx(
data: string,
to: string,
chainId: number
): Promise<FunctionSignature | null> {
try {
const network = SAFE_NETWORKS[chainId] || SAFE_NETWORKS[1];
const response = await fetch(
`https://safe-transaction-${network}.safe.global/api/v1/data-decoder/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
accept: "application/json",
},
body: JSON.stringify({ data, to }),
}
);

// Handle different response status codes
if (response.status === 404) {
console.warn("Cannot find function selector to decode data");
return null;
}

if (response.status === 422) {
const errorData = (await response.json()) as SafeDecoderErrorResponse;
console.error("Invalid data:", errorData.message, errorData.arguments);
return null;
}

if (!response.ok) {
console.error(`Unexpected response status: ${response.status}`);
return null;
}

return (await response.json()) as FunctionSignature;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error decoding transaction:", message);
return null;
}
}

export const formatEvmData = (
decodedEvmData: DecodedTxData,
functionSignatures: (FunctionSignature | null)[] = []
): string => {
const formatted = {
...decodedEvmData,
network: Network.fromChainId(decodedEvmData.chainId).name,
functionSignatures,
};

return JSON.stringify(formatted, bigIntReplacer, 2);
};

/**
* Replaces bigint values with their string representation.
*/
const bigIntReplacer = (_: string, value: unknown): unknown =>
typeof value === "bigint" ? value.toString() : value;
60 changes: 2 additions & 58 deletions src/decode/index.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,2 @@
import { isRlpHex, isTransactionSerializable } from "near-ca";

import {
DecodedTxData,
parseEip712TypedData,
parseUserOperation,
SafeEncodedSignRequest,
} from "../types";
import {
decodeRlpHex,
decodeTransactionSerializable,
decodeTypedData,
decodeUserOperation,
} from "./util";

/**
* Decodes transaction data for a given EVM transaction and extracts relevant details.
*
* @param {EvmTransactionData} data - The raw transaction data to be decoded.
* @returns {DecodedTxData} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
*/
export function decodeTxData({
evmMessage,
chainId,
}: Omit<SafeEncodedSignRequest, "hashToSign">): DecodedTxData {
const data = evmMessage;
if (isRlpHex(evmMessage)) {
return decodeRlpHex(chainId, evmMessage);
}
if (isTransactionSerializable(data)) {
return decodeTransactionSerializable(chainId, data);
}
const parsedTypedData = parseEip712TypedData(data);
if (parsedTypedData) {
return decodeTypedData(chainId, parsedTypedData);
}
const userOp = parseUserOperation(data);
if (userOp) {
return decodeUserOperation(chainId, userOp);
}
// At this point we are certain that the data is a string.
// Typescript would disagree here because of the EIP712TypedData possibility that remains.
// However this is captured (indirectly) by parseEip712TypedData above.
// We check now if its a string and return a reasonable default (for the case of a raw message).
if (typeof data === "string") {
return {
chainId,
costEstimate: "0",
transactions: [],
message: data,
};
}
// Otherwise we have no idea what the data is and we throw.
console.warn("Unrecognized txData format,", chainId, data);
throw new Error(
`decodeTxData: Invalid or unsupported message format ${data}`
);
}
export * from "./explain";
export * from "./sign-request";
58 changes: 58 additions & 0 deletions src/decode/sign-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { isRlpHex, isTransactionSerializable } from "near-ca";

import {
DecodedTxData,
parseEip712TypedData,
parseUserOperation,
SafeEncodedSignRequest,
} from "../types";
import {
decodeRlpHex,
decodeTransactionSerializable,
decodeTypedData,
decodeUserOperation,
} from "./util";

/**
* Decodes transaction data for a given EVM transaction and extracts relevant details.
*
* @param {EvmTransactionData} data - The raw transaction data to be decoded.
* @returns {DecodedTxData} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
*/
export function decodeTxData({
evmMessage,
chainId,
}: Omit<SafeEncodedSignRequest, "hashToSign">): DecodedTxData {
const data = evmMessage;
if (isRlpHex(evmMessage)) {
return decodeRlpHex(chainId, evmMessage);
}
if (isTransactionSerializable(data)) {
return decodeTransactionSerializable(chainId, data);
}
const parsedTypedData = parseEip712TypedData(data);
if (parsedTypedData) {
return decodeTypedData(chainId, parsedTypedData);
}
const userOp = parseUserOperation(data);
if (userOp) {
return decodeUserOperation(chainId, userOp);
}
// At this point we are certain that the data is a string.
// Typescript would disagree here because of the EIP712TypedData possibility that remains.
// However this is captured (indirectly) by parseEip712TypedData above.
// We check now if its a string and return a reasonable default (for the case of a raw message).
if (typeof data === "string") {
return {
chainId,
costEstimate: "0",
transactions: [],
message: data,
};
}
// Otherwise we have no idea what the data is and we throw.
console.warn("Unrecognized txData format,", chainId, data);
throw new Error(
`decodeTxData: Invalid or unsupported message format ${data}`
);
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export * from "./near-safe";
export * from "./types";
export * from "./util";
export * from "./constants";
export { decodeTxData } from "./decode";
export * from "./decode";
export * from "./lib/safe-message";

export {
Expand Down
2 changes: 1 addition & 1 deletion src/near-safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export class NearSafe {
): Promise<MetaTransaction> {
return {
to: this.address,
value: "0",
value: "0x00",
data: await this.safePack.removeOwnerData(chainId, this.address, address),
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export interface MetaTransaction {
/** The destination address for the meta-transaction. */
readonly to: string;
/** The value to be sent with the transaction (as a string to handle large numbers). */
readonly value: string;
readonly value: string; // TODO: Change to hex string! No Confusion.
/** The encoded data for the contract call or function execution. */
readonly data: string;
/** Optional type of operation (call or delegate call). */
Expand Down
2 changes: 1 addition & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function packPaymasterData(data: PaymasterData): Hex {
}

export function containsValue(transactions: MetaTransaction[]): boolean {
return transactions.some((tx) => tx.value !== "0");
return transactions.some((tx) => BigInt(tx.value) !== 0n);
}

export async function isContract(
Expand Down
Loading