Skip to content

Commit

Permalink
Explain SignRequest (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Nov 14, 2024
1 parent 3bfeaef commit 9da465a
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 272 deletions.
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

0 comments on commit 9da465a

Please sign in to comment.